diff --git a/Makefile b/Makefile index 84c4e11f400..0d3eac4ceb5 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,8 @@ dist: benchmark: @go test -bench=. ./modules/... -test: test_unit test_cli test_tutorial +#test: test_unit test_cli test_tutorial +test: test_unit test_cli test_unit: @go test `glide novendor` @@ -27,7 +28,11 @@ 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/roles.sh ./tests/cli/counter.sh ./tests/cli/restart.sh # @./tests/cli/ibc.sh diff --git a/app/app.go b/app/app.go index bdcdbbbd80f..378b36e0b21 100644 --- a/app/app.go +++ b/app/app.go @@ -16,6 +16,7 @@ import ( "github.com/tendermint/basecoin/modules/coin" "github.com/tendermint/basecoin/modules/fee" "github.com/tendermint/basecoin/modules/nonce" + "github.com/tendermint/basecoin/modules/roles" "github.com/tendermint/basecoin/stack" sm "github.com/tendermint/basecoin/state" "github.com/tendermint/basecoin/version" @@ -56,14 +57,19 @@ func NewBasecoin(handler basecoin.Handler, eyesCli *eyes.Client, logger log.Logg // DefaultHandler - placeholder to just handle sendtx func DefaultHandler(feeDenom string) basecoin.Handler { // use the default stack - h := coin.NewHandler() - d := stack.NewDispatcher(stack.WrapHandler(h)) + c := coin.NewHandler() + r := roles.NewHandler() + d := stack.NewDispatcher( + stack.WrapHandler(c), + stack.WrapHandler(r), + ) return stack.New( base.Logger{}, stack.Recovery{}, auth.Signatures{}, base.Chain{}, nonce.ReplayCheck{}, + roles.NewMiddleware(), fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank), ).Use(d) } @@ -139,7 +145,9 @@ func (app *Basecoin) CheckTx(txBytes []byte) abci.Result { return errors.Result(err) } - // TODO: can we abstract this setup and commit logic?? + // we also need to discard error changes, so we don't increment checktx + // sequence on error, but not delivertx + cache := app.cacheState.CacheWrap() ctx := stack.NewContext( app.state.GetChainID(), app.height, @@ -147,11 +155,12 @@ func (app *Basecoin) CheckTx(txBytes []byte) abci.Result { ) // checktx generally shouldn't touch the state, but we don't care // here on the framework level, since the cacheState is thrown away next block - res, err := app.handler.CheckTx(ctx, app.cacheState, tx) + res, err := app.handler.CheckTx(ctx, cache, tx) if err != nil { return errors.Result(err) } + cache.CacheSync() return res.ToABCI() } diff --git a/app/genesis_test.go b/app/genesis_test.go index 95e1d788e34..57c895c8d73 100644 --- a/app/genesis_test.go +++ b/app/genesis_test.go @@ -7,10 +7,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/basecoin/modules/coin" + eyescli "github.com/tendermint/merkleeyes/client" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/basecoin/modules/coin" ) const genesisFilepath = "./testdata/genesis.json" 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 040eb6ac38f..704ba178517 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 00000000000..a7ea8da7d70 --- /dev/null +++ b/client/commands/common.go @@ -0,0 +1,141 @@ +/* +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 +} + +// ParseActor parses an address of form: +// [:][:] +// into a basecoin.Actor. +// If app is not specified or "", then assume auth.NameSigs +func ParseActor(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 +} + +// ParseActors takes a comma-separated list of actors and parses them into +// a slice +func ParseActors(key string) (signers []basecoin.Actor, err error) { + var act basecoin.Actor + for _, k := range strings.Split(key, ",") { + act, err = ParseActor(k) + if err != nil { + return + } + signers = append(signers, act) + } + return +} + +// GetOneArg makes sure there is exactly one positional argument +func GetOneArg(args []string, argname string) (string, error) { + if len(args) == 0 { + return "", errors.Errorf("Missing required argument [%s]", argname) + } + if len(args) > 1 { + return "", errors.Errorf("Only accepts one argument [%s]", argname) + } + return args[0], nil +} diff --git a/client/commands/init.go b/client/commands/init.go new file mode 100644 index 00000000000..fe0109372a5 --- /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/light-client/certifiers" + "github.com/tendermint/light-client/certifiers/files" + "github.com/tendermint/tmlibs/cli" + cmn "github.com/tendermint/tmlibs/common" + + "github.com/tendermint/tendermint/types" +) + +var ( + dirPerm = os.FileMode(0700) +) + +//nolint +const ( + 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 00000000000..34638698cce --- /dev/null +++ b/client/commands/proofs/get.go @@ -0,0 +1,118 @@ +package proofs + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/viper" + + wire "github.com/tendermint/go-wire" + "github.com/tendermint/go-wire/data" + lc "github.com/tendermint/light-client" + "github.com/tendermint/light-client/proofs" + "github.com/tendermint/tendermint/rpc/client" + + "github.com/tendermint/basecoin/client/commands" +) + +// 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 00000000000..15b49b8c907 --- /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 00000000000..e55b5b58103 --- /dev/null +++ b/client/commands/proofs/state.go @@ -0,0 +1,47 @@ +package proofs + +import ( + "github.com/spf13/cobra" + + "github.com/tendermint/go-wire/data" + "github.com/tendermint/light-client/proofs" + + "github.com/tendermint/basecoin/client/commands" +) + +// KeyQueryCmd - CLI command to query a state by key with proof +var KeyQueryCmd = &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(keyQueryCmd), +} + +// Note: we cannot yse GetAndParseAppProof here, as we don't use go-wire to +// parse the object, but rather return the raw bytes +func keyQueryCmd(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 00000000000..cffc4dfc11b --- /dev/null +++ b/client/commands/proofs/tx.go @@ -0,0 +1,52 @@ +package proofs + +import ( + "github.com/spf13/cobra" + + "github.com/tendermint/light-client/proofs" + + "github.com/tendermint/basecoin/client/commands" +) + +//nolint TODO add description +var TxPresenters = proofs.NewPresenters() + +// TxQueryCmd - CLI command to query a transaction with proof +var TxQueryCmd = &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(txQueryCmd), +} + +func txQueryCmd(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 00000000000..4845773dad0 --- /dev/null +++ b/client/commands/proxy/root.go @@ -0,0 +1,110 @@ +package proxy + +import ( + "net/http" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + certclient "github.com/tendermint/light-client/certifiers/client" + "github.com/tendermint/tendermint/rpc/client" + "github.com/tendermint/tendermint/rpc/core" + rpc "github.com/tendermint/tendermint/rpc/lib/server" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + + "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 00000000000..5ef0f4f3ef1 --- /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 00000000000..5587a591406 --- /dev/null +++ b/client/commands/rpc/insecure.go @@ -0,0 +1,67 @@ +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 00000000000..2a1d0e532bf --- /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" + certclient "github.com/tendermint/light-client/certifiers/client" + "github.com/tendermint/tendermint/rpc/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 00000000000..717c20c9814 --- /dev/null +++ b/client/commands/rpc/secure.go @@ -0,0 +1,76 @@ +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 00000000000..1ac3ac42a80 --- /dev/null +++ b/client/commands/seeds/export.go @@ -0,0 +1,44 @@ +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 00000000000..4a63ef7d78a --- /dev/null +++ b/client/commands/seeds/import.go @@ -0,0 +1,59 @@ +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 00000000000..b4fc9d66f6c --- /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 00000000000..100201b6370 --- /dev/null +++ b/client/commands/seeds/show.go @@ -0,0 +1,73 @@ +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 00000000000..153f090f8c9 --- /dev/null +++ b/client/commands/seeds/update.go @@ -0,0 +1,44 @@ +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 00000000000..68e53915ff5 --- /dev/null +++ b/client/commands/txs/helpers.go @@ -0,0 +1,245 @@ +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" + wire "github.com/tendermint/go-wire" + "github.com/tendermint/go-wire/data" + + 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(FlagName) + 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 +} + +// DoTx is a helper function for the lazy :) +// +// It uses only public functions and goes through the standard sequence of +// wrapping the tx with middleware layers, signing it, either preparing it, +// or posting it and displaying the result. +// +// If you want a non-standard flow, just call the various functions directly. +// eg. if you already set the middleware layers in your code, or want to +// output in another format. +func DoTx(tx basecoin.Tx) (err error) { + tx, err = Middleware.Wrap(tx) + if err != nil { + return err + } + + err = SignTx(tx) + if err != nil { + return err + } + + bres, err := PrepareOrPostTx(tx) + if err != nil { + return err + } + if bres == nil { + return nil // successful prep, nothing left to do + } + return OutputTx(bres) // print response of the post + +} + +// SignTx will validate the tx, and signs it if it is wrapping a Signable. +// Modifies tx in place, and returns an error if it should sign but couldn't +func SignTx(tx basecoin.Tx) error { + // validate tx client-side + err := tx.ValidateBasic() + if err != nil { + return err + } + + name := viper.GetString(FlagName) + manager := keycmd.GetKeyManager() + + if sign, ok := tx.Unwrap().(keys.Signable); ok { + // TODO: allow us not to sign? if so then what use? + if name == "" { + return errors.New("--name is required to sign tx") + } + err = signTx(manager, sign, name) + } + return err +} + +// PrepareOrPostTx checks the flags to decide to prepare the tx for future +// multisig, or to post it to the node. Returns error on any failure. +// If no error and the result is nil, it means it already wrote to file, +// no post, no need to do more. +func PrepareOrPostTx(tx basecoin.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + wrote, err := PrepareTx(tx) + // error in prep + if err != nil { + return nil, err + } + // successfully wrote the tx! + if wrote { + return nil, nil + } + // or try to post it + return PostTx(tx) +} + +// PrepareTx checks for FlagPrepare and if set, write the tx as json +// to the specified location for later multi-sig. Returns true if it +// handled the tx (no futher work required), false if it did nothing +// (and we should post the tx) +func PrepareTx(tx basecoin.Tx) (bool, error) { + prep := viper.GetString(FlagPrepare) + if prep == "" { + return false, nil + } + + js, err := data.ToJSON(tx) + if err != nil { + return false, err + } + err = writeOutput(prep, js) + if err != nil { + return false, err + } + return true, nil +} + +// PostTx 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 PostTx(tx basecoin.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + packet := wire.BinaryBytes(tx) + // post the bytes + node := commands.GetNode() + return node.BroadcastTxCommit(packet) +} + +// OutputTx validates if success and prints the tx result to stdout +func OutputTx(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) + } + 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) error { + prompt := fmt.Sprintf("Please enter passphrase for %s: ", name) + pass, err := getPassword(prompt) + if err != nil { + return err + } + return manager.Sign(name, pass, tx) +} + +// 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 +} + +func writeOutput(file string, d []byte) error { + var writer io.Writer + if file == "-" { + writer = os.Stdout + } else { + f, err := os.Create(file) + if err != nil { + return errors.WithStack(err) + } + defer f.Close() + writer = f + } + + _, err := writer.Write(d) + // this returns nil if err == nil + return errors.WithStack(err) +} + +func readInput(file string) ([]byte, error) { + var reader io.Reader + // get the input stream + if file == "" || file == "-" { + reader = os.Stdin + } else { + f, err := os.Open(file) + if err != nil { + return nil, errors.WithStack(err) + } + defer f.Close() + reader = f + } + + // and read it all! + data, err := ioutil.ReadAll(reader) + return data, errors.WithStack(err) +} diff --git a/client/commands/txs/presenter.go b/client/commands/txs/presenter.go new file mode 100644 index 00000000000..2886e7bd2f3 --- /dev/null +++ b/client/commands/txs/presenter.go @@ -0,0 +1,20 @@ +package txs + +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/client/commands/txs/root.go b/client/commands/txs/root.go new file mode 100644 index 00000000000..e2b88a6e68a --- /dev/null +++ b/client/commands/txs/root.go @@ -0,0 +1,62 @@ +package txs + +import ( + "encoding/json" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin" +) + +// nolint +const ( + FlagName = "name" + FlagIn = "in" + FlagPrepare = "prepare" +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "tx", + Short: "Post tx from json input", + RunE: doRawTx, +} + +func init() { + RootCmd.PersistentFlags().String(FlagName, "", "name to sign the tx") + // TODO: prepare needs to override the SignAndPost somehow to SignAndSave + RootCmd.PersistentFlags().String(FlagPrepare, "", "file to store prepared tx") + RootCmd.Flags().String(FlagIn, "", "file with tx in json format") +} + +func doRawTx(cmd *cobra.Command, args []string) error { + raw, err := readInput(viper.GetString(FlagIn)) + if err != nil { + return err + } + + // parse the input + var tx basecoin.Tx + err = json.Unmarshal(raw, &tx) + if err != nil { + return errors.WithStack(err) + } + + // sign it + err = SignTx(tx) + if err != nil { + return err + } + + // otherwise, post it and display response + bres, err := PrepareOrPostTx(tx) + if err != nil { + return err + } + if bres == nil { + return nil // successful prep, nothing left to do + } + return OutputTx(bres) // print response of the post +} diff --git a/client/commands/txs/wrapper.go b/client/commands/txs/wrapper.go new file mode 100644 index 00000000000..3d711f948b6 --- /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/client/commands/version.go b/client/commands/version.go new file mode 100644 index 00000000000..9ffa6b36dc8 --- /dev/null +++ b/client/commands/version.go @@ -0,0 +1,17 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/tendermint/basecoin/version" +) + +// VersionCmd - command to show the application version +var VersionCmd = &cobra.Command{ + Use: "version", + Short: "Show version info", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(version.Version) + }, +} diff --git a/cmd/basecli/commands/cmds.go b/cmd/basecli/commands/cmds.go deleted file mode 100644 index 3732f38fc08..00000000000 --- a/cmd/basecli/commands/cmds.go +++ /dev/null @@ -1,220 +0,0 @@ -package commands - -import ( - "encoding/hex" - "fmt" - "strings" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/tendermint/light-client/commands" - 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" - "github.com/tendermint/basecoin/modules/base" - "github.com/tendermint/basecoin/modules/coin" - "github.com/tendermint/basecoin/modules/fee" - "github.com/tendermint/basecoin/modules/nonce" -) - -//------------------------- -// SendTx - -// SendTxCmd is CLI command to send tokens between basecoin accounts -var SendTxCmd = &cobra.Command{ - Use: "send", - Short: "send tokens from one account to another", - RunE: commands.RequireInit(doSendTx), -} - -//nolint -const ( - FlagTo = "to" - FlagAmount = "amount" - FlagFee = "fee" - FlagGas = "gas" - FlagExpires = "expires" - FlagSequence = "sequence" -) - -func init() { - flags := SendTxCmd.Flags() - flags.String(FlagTo, "", "Destination address for the bits") - flags.String(FlagAmount, "", "Coins to send in the format ,...") - flags.String(FlagFee, "0mycoin", "Coins for the transaction fee of the format ") - flags.Uint64(FlagGas, 0, "Amount of gas for this transaction") - flags.Uint64(FlagExpires, 0, "Block height at which this tx expires") - flags.Int(FlagSequence, -1, "Sequence number for this transaction") -} - -// doSendTx is an example of how to make a tx -func doSendTx(cmd *cobra.Command, args []string) error { - // load data from json or flags - var tx basecoin.Tx - found, err := txcmd.LoadJSON(&tx) - if err != nil { - return err - } - if !found { - tx, err = readSendTxFlags() - } - if err != nil { - return err - } - - // TODO: make this more flexible for middleware - tx, err = WrapFeeTx(tx) - if err != nil { - return err - } - tx, err = WrapNonceTx(tx) - if err != nil { - return err - } - tx, err = WrapChainTx(tx) - if err != nil { - return err - } - - // Note: this is single sig (no multi sig yet) - stx := auth.NewSig(tx) - - // Sign if needed and post. This it the work-horse - bres, err := txcmd.SignAndPostTx(stx) - if err != nil { - return err - } - if err = ValidateResult(bres); err != nil { - return err - } - - // Output result - return txcmd.OutputTx(bres) -} - -// 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 -} - -// WrapNonceTx grabs the sequence number from the flag and wraps -// the tx with this nonce. Grabs the permission from the signer, -// as we still only support single sig on the cli -func WrapNonceTx(tx basecoin.Tx) (res basecoin.Tx, err error) { - //add the nonce tx layer to the tx - seq := viper.GetInt(FlagSequence) - if seq < 0 { - return res, fmt.Errorf("sequence must be greater than 0") - } - signers := []basecoin.Actor{GetSignerAct()} - res = nonce.NewTx(uint32(seq), signers, tx) - return -} - -// WrapFeeTx checks for FlagFee and if present wraps the tx with a -// FeeTx of the given amount, paid by the signer -func WrapFeeTx(tx basecoin.Tx) (res basecoin.Tx, err error) { - //parse the fee and amounts into coin types - toll, err := coin.ParseCoin(viper.GetString(FlagFee)) - if err != nil { - return res, err - } - // if no fee, do nothing, otherwise wrap it - if toll.IsZero() { - return tx, nil - } - res = fee.NewFee(tx, toll, GetSignerAct()) - return -} - -// WrapChainTx will wrap the tx with a ChainTx from the standard flags -func WrapChainTx(tx basecoin.Tx) (res basecoin.Tx, err error) { - expires := viper.GetInt64(FlagExpires) - chain := commands.GetChainID() - if chain == "" { - return res, errors.New("No chain-id provided") - } - res = base.NewChainTx(chain, uint64(expires), tx) - 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 -} - -func readSendTxFlags() (tx basecoin.Tx, err error) { - // parse to address - chain, to, err := parseChainAddress(viper.GetString(FlagTo)) - if err != nil { - return tx, err - } - toAddr := auth.SigPerm(to) - toAddr.ChainID = chain - - amountCoins, err := coin.ParseCoins(viper.GetString(FlagAmount)) - if err != nil { - return tx, err - } - - // craft the inputs and outputs - ins := []coin.TxInput{{ - Address: GetSignerAct(), - Coins: amountCoins, - }} - outs := []coin.TxOutput{{ - Address: toAddr, - Coins: amountCoins, - }} - - return coin.NewSendTx(ins, outs), nil -} - -func parseChainAddress(toFlag string) (string, []byte, error) { - var toHex string - var chainPrefix string - spl := strings.Split(toFlag, "/") - switch len(spl) { - case 1: - toHex = spl[0] - case 2: - chainPrefix = spl[0] - toHex = spl[1] - default: - return "", nil, errors.Errorf("To address has too many slashes") - } - - // convert destination address to bytes - to, err := hex.DecodeString(cmn.StripHex(toHex)) - if err != nil { - return "", nil, errors.Errorf("To address is invalid hex: %v\n", err) - } - - return chainPrefix, to, nil -} - -/** TODO copied from basecoin cli - put in common somewhere? **/ - -// ParseHexFlag parses a flag string to byte array -func ParseHexFlag(flag string) ([]byte, error) { - return hex.DecodeString(cmn.StripHex(viper.GetString(flag))) -} diff --git a/cmd/basecli/commands/query.go b/cmd/basecli/commands/query.go deleted file mode 100644 index ce3fa338cac..00000000000 --- a/cmd/basecli/commands/query.go +++ /dev/null @@ -1,86 +0,0 @@ -package commands - -import ( - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "github.com/tendermint/basecoin" - wire "github.com/tendermint/go-wire" - lc "github.com/tendermint/light-client" - lcmd "github.com/tendermint/light-client/commands" - proofcmd "github.com/tendermint/light-client/commands/proofs" - "github.com/tendermint/light-client/proofs" - - "github.com/tendermint/basecoin/modules/auth" - "github.com/tendermint/basecoin/modules/coin" - "github.com/tendermint/basecoin/modules/nonce" - "github.com/tendermint/basecoin/stack" -) - -// AccountQueryCmd - command to query an account -var AccountQueryCmd = &cobra.Command{ - Use: "account [address]", - Short: "Get details of an account, with proof", - RunE: lcmd.RequireInit(doAccountQuery), -} - -func doAccountQuery(cmd *cobra.Command, args []string) error { - addr, err := proofcmd.ParseHexKey(args, "address") - if err != nil { - return err - } - key := stack.PrefixedKey(coin.NameCoin, auth.SigPerm(addr).Bytes()) - - acc := coin.Account{} - proof, err := proofcmd.GetAndParseAppProof(key, &acc) - if lc.IsNoDataErr(err) { - return errors.Errorf("Account bytes are empty for address %X ", addr) - } else if err != nil { - return err - } - - return proofcmd.OutputProof(acc, proof.BlockHeight()) -} - -// NonceQueryCmd - command to query an nonce account -var NonceQueryCmd = &cobra.Command{ - Use: "nonce [address]", - Short: "Get details of a nonce sequence number, with proof", - RunE: lcmd.RequireInit(doNonceQuery), -} - -func doNonceQuery(cmd *cobra.Command, args []string) error { - addr, err := proofcmd.ParseHexKey(args, "address") - if err != nil { - return err - } - - act := []basecoin.Actor{basecoin.NewActor( - auth.NameSigs, - addr, - )} - - key := stack.PrefixedKey(nonce.NameNonce, nonce.GetSeqKey(act)) - - var seq uint32 - proof, err := proofcmd.GetAndParseAppProof(key, &seq) - if lc.IsNoDataErr(err) { - return errors.Errorf("Sequence is empty for address %X ", addr) - } else if err != nil { - return err - } - - return proofcmd.OutputProof(seq, proof.BlockHeight()) -} - -// 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 48dcf1dd59b..02335e39046 100644 --- a/cmd/basecli/main.go +++ b/cmd/basecli/main.go @@ -6,16 +6,21 @@ 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" - 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" - coincmd "github.com/tendermint/basecoin/cmd/basecoin/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" + feecmd "github.com/tendermint/basecoin/modules/fee/commands" + noncecmd "github.com/tendermint/basecoin/modules/nonce/commands" + rolecmd "github.com/tendermint/basecoin/modules/roles/commands" ) // BaseCli - main basecoin client command @@ -36,17 +41,30 @@ func main() { // Prepare queries proofs.RootCmd.AddCommand( // These are default parsers, but optional in your app (you can remove key) - proofs.TxCmd, - proofs.KeyCmd, - bcmd.AccountQueryCmd, - bcmd.NonceQueryCmd, + proofs.TxQueryCmd, + proofs.KeyQueryCmd, + coincmd.AccountQueryCmd, + noncecmd.NonceQueryCmd, + rolecmd.RoleQueryCmd, ) + proofs.TxPresenters.Register("base", txcmd.BaseTxPresenter{}) + + // set up the middleware + txcmd.Middleware = txcmd.Wrappers{ + feecmd.FeeWrapper{}, + rolecmd.RoleWrapper{}, + noncecmd.NonceWrapper{}, + basecmd.ChainWrapper{}, + authcmd.SigWrapper{}, + } + 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 - bcmd.SendTxCmd, + coincmd.SendTxCmd, + // this enables creating roles + rolecmd.CreateRoleTxCmd, ) // Set up the various commands to use @@ -57,10 +75,10 @@ func main() { seeds.RootCmd, rpccmd.RootCmd, proofs.RootCmd, - txs.RootCmd, + txcmd.RootCmd, proxy.RootCmd, - coincmd.VersionCmd, - bcmd.AutoCompleteCmd, + commands.VersionCmd, + auto.AutoCompleteCmd, ) cmd := cli.PrepareMainCmd(BaseCli, "BC", os.ExpandEnv("$HOME/.basecli")) diff --git a/cmd/basecoin/commands/key.go b/cmd/basecoin/commands/key.go index b5b03e3fba8..72620072f52 100644 --- a/cmd/basecoin/commands/key.go +++ b/cmd/basecoin/commands/key.go @@ -8,8 +8,6 @@ import ( "path" "strings" - //"github.com/pkg/errors" - "github.com/spf13/viper" "github.com/tendermint/go-crypto" diff --git a/cmd/basecoin/main.go b/cmd/basecoin/main.go index 8b4f5764cd7..33ba8c1d488 100644 --- a/cmd/basecoin/main.go +++ b/cmd/basecoin/main.go @@ -3,9 +3,10 @@ package main import ( "os" + "github.com/tendermint/tmlibs/cli" + "github.com/tendermint/basecoin/app" "github.com/tendermint/basecoin/cmd/basecoin/commands" - "github.com/tendermint/tmlibs/cli" ) func main() { diff --git a/docs/guide/counter/cmd/countercli/commands/counter.go b/docs/guide/counter/cmd/countercli/commands/counter.go index 68fb867006d..cf4dbfe28df 100644 --- a/docs/guide/counter/cmd/countercli/commands/counter.go +++ b/docs/guide/counter/cmd/countercli/commands/counter.go @@ -4,12 +4,9 @@ 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/auth" "github.com/tendermint/basecoin/modules/coin" ) @@ -34,54 +31,14 @@ func init() { fs := CounterTxCmd.Flags() fs.String(FlagCountFee, "", "Coins to send in the format ,...") fs.Bool(FlagValid, false, "Is count valid?") - - fs.String(bcmd.FlagFee, "0mycoin", "Coins for the transaction fee of the format ") - fs.Int(bcmd.FlagSequence, -1, "Sequence number for this transaction") } -// TODO: counterTx is very similar to the sendtx one, -// maybe we can pull out some common patterns? func counterTx(cmd *cobra.Command, args []string) error { - // load data from json or flags - var tx basecoin.Tx - found, err := txcmd.LoadJSON(&tx) - if err != nil { - return err - } - if !found { - tx, err = readCounterTxFlags() - } - if err != nil { - return err - } - - // TODO: make this more flexible for middleware - tx, err = bcmd.WrapFeeTx(tx) - if err != nil { - return err - } - tx, err = bcmd.WrapNonceTx(tx) - if err != nil { - return err - } - tx, err = bcmd.WrapChainTx(tx) - if err != nil { - return err - } - - stx := auth.NewSig(tx) - - // Sign if needed and post. This it the work-horse - bres, err := txcmd.SignAndPostTx(stx) + tx, err := readCounterTxFlags() if err != nil { return err } - if err = bcmd.ValidateResult(bres); err != nil { - return err - } - - // Output result - return txcmd.OutputTx(bres) + return txcmd.DoTx(tx) } func readCounterTxFlags() (tx basecoin.Tx, err error) { diff --git a/docs/guide/counter/cmd/countercli/commands/query.go b/docs/guide/counter/cmd/countercli/commands/query.go index 489a8a01d54..8debaf9c2b8 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 7b543d35266..6e930cd7d53 100644 --- a/docs/guide/counter/cmd/countercli/main.go +++ b/docs/guide/counter/cmd/countercli/main.go @@ -6,15 +6,19 @@ 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" + coincmd "github.com/tendermint/basecoin/modules/coin/commands" + feecmd "github.com/tendermint/basecoin/modules/fee/commands" + noncecmd "github.com/tendermint/basecoin/modules/nonce/commands" ) // BaseCli represents the base command when called without any subcommands @@ -35,19 +39,29 @@ func main() { // Prepare queries proofs.RootCmd.AddCommand( // These are default parsers, optional in your app - proofs.TxCmd, - proofs.KeyCmd, - bcmd.AccountQueryCmd, + proofs.TxQueryCmd, + proofs.KeyQueryCmd, + coincmd.AccountQueryCmd, + noncecmd.NonceQueryCmd, // XXX IMPORTANT: here is how you add custom query commands in your app bcount.CounterQueryCmd, ) + // set up the middleware + txcmd.Middleware = txcmd.Wrappers{ + feecmd.FeeWrapper{}, + noncecmd.NonceWrapper{}, + basecmd.ChainWrapper{}, + authcmd.SigWrapper{}, + } + 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 - bcmd.SendTxCmd, + coincmd.SendTxCmd, // XXX IMPORTANT: here is how you add custom tx construction for your app bcount.CounterTxCmd, @@ -60,7 +74,7 @@ func main() { keycmd.RootCmd, seeds.RootCmd, proofs.RootCmd, - txs.RootCmd, + txcmd.RootCmd, proxy.RootCmd, ) diff --git a/errors/common.go b/errors/common.go index 1ef42177f9f..f4fd66debf3 100644 --- a/errors/common.go +++ b/errors/common.go @@ -5,27 +5,17 @@ import ( "fmt" "reflect" - "github.com/pkg/errors" - abci "github.com/tendermint/abci/types" ) var ( - errDecoding = fmt.Errorf("Error decoding input") - errUnauthorized = fmt.Errorf("Unauthorized") - errInvalidSignature = fmt.Errorf("Invalid Signature") - errTooLarge = fmt.Errorf("Input size too large") - errNoSigners = fmt.Errorf("There are no signers") - errMissingSignature = fmt.Errorf("Signature missing") - errTooManySignatures = fmt.Errorf("Too many signatures") - errNoChain = fmt.Errorf("No chain id provided") - errTxEmpty = fmt.Errorf("The provided Tx is empty") - errWrongChain = fmt.Errorf("Wrong chain for tx") - errUnknownTxType = fmt.Errorf("Tx type unknown") - errInvalidFormat = fmt.Errorf("Invalid format") - errUnknownModule = fmt.Errorf("Unknown module") - errExpired = fmt.Errorf("Tx expired") - errUnknownKey = fmt.Errorf("Unknown key") + errDecoding = fmt.Errorf("Error decoding input") + errUnauthorized = fmt.Errorf("Unauthorized") + errTooLarge = fmt.Errorf("Input size too large") + errMissingSignature = fmt.Errorf("Signature missing") + errUnknownTxType = fmt.Errorf("Tx type unknown") + errInvalidFormat = fmt.Errorf("Invalid format") + errUnknownModule = fmt.Errorf("Unknown module") internalErr = abci.CodeType_InternalError encodingErr = abci.CodeType_EncodingError @@ -70,14 +60,6 @@ func IsUnknownModuleErr(err error) bool { return IsSameError(errUnknownModule, err) } -func ErrUnknownKey(mod string) TMError { - w := errors.Wrap(errUnknownKey, mod) - return WithCode(w, abci.CodeType_UnknownRequest) -} -func IsUnknownKeyErr(err error) bool { - return IsSameError(errUnknownKey, err) -} - func ErrInternal(msg string) TMError { return New(msg, internalErr) } @@ -104,10 +86,6 @@ func IsUnauthorizedErr(err error) bool { return HasErrorCode(err, unauthorized) } -func ErrNoSigners() TMError { - return WithCode(errNoSigners, unauthorized) -} - func ErrMissingSignature() TMError { return WithCode(errMissingSignature, unauthorized) } @@ -115,49 +93,9 @@ func IsMissingSignatureErr(err error) bool { return IsSameError(errMissingSignature, err) } -func ErrTooManySignatures() TMError { - return WithCode(errTooManySignatures, unauthorized) -} -func IsTooManySignaturesErr(err error) bool { - return IsSameError(errTooManySignatures, err) -} - -func ErrInvalidSignature() TMError { - return WithCode(errInvalidSignature, unauthorized) -} -func IsInvalidSignatureErr(err error) bool { - return IsSameError(errInvalidSignature, err) -} - -func ErrNoChain() TMError { - return WithCode(errNoChain, unauthorized) -} -func IsNoChainErr(err error) bool { - return IsSameError(errNoChain, err) -} - -func ErrTxEmpty() TMError { - return WithCode(errTxEmpty, unauthorized) -} - -func ErrWrongChain(chain string) TMError { - msg := errors.Wrap(errWrongChain, chain) - return WithCode(msg, unauthorized) -} -func IsWrongChainErr(err error) bool { - return IsSameError(errWrongChain, err) -} - func ErrTooLarge() TMError { return WithCode(errTooLarge, encodingErr) } func IsTooLargeErr(err error) bool { return IsSameError(errTooLarge, err) } - -func ErrExpired() TMError { - return WithCode(errExpired, unauthorized) -} -func IsExpiredErr(err error) bool { - return IsSameError(errExpired, err) -} diff --git a/errors/common_test.go b/errors/common_test.go index 59d5fc62680..f16933df4d3 100644 --- a/errors/common_test.go +++ b/errors/common_test.go @@ -42,7 +42,6 @@ func TestErrorMatches(t *testing.T) { {errUnauthorized, ErrUnauthorized(), true}, {errMissingSignature, ErrUnauthorized(), false}, {errMissingSignature, ErrMissingSignature(), true}, - {errWrongChain, ErrWrongChain("hakz"), true}, {errUnknownTxType, ErrUnknownTxType(holder{}), true}, {errUnknownTxType, ErrUnknownTxType("some text here..."), true}, {errUnknownTxType, ErrUnknownTxType(demoTx{5}.Wrap()), true}, @@ -66,13 +65,6 @@ func TestChecks(t *testing.T) { {ErrDecoding(), IsDecodingErr, true}, {ErrUnauthorized(), IsDecodingErr, false}, {ErrUnauthorized(), IsUnauthorizedErr, true}, - {ErrInvalidSignature(), IsInvalidSignatureErr, true}, - // unauthorized includes InvalidSignature, but not visa versa - {ErrInvalidSignature(), IsUnauthorizedErr, true}, - {ErrUnauthorized(), IsInvalidSignatureErr, false}, - // make sure WrongChain works properly - {ErrWrongChain("fooz"), IsUnauthorizedErr, true}, - {ErrWrongChain("barz"), IsWrongChainErr, true}, // make sure lots of things match InternalErr, but not everything {ErrInternal("bad db connection"), IsInternalErr, true}, {Wrap(errors.New("wrapped")), IsInternalErr, true}, diff --git a/glide.lock b/glide.lock index 71ebac8c7a5..9157e3fc72c 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 7e7727aac48..d9072f48244 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 new file mode 100644 index 00000000000..c93eecb7023 --- /dev/null +++ b/modules/auth/commands/wrap.go @@ -0,0 +1,35 @@ +package commands + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin" + txcmd "github.com/tendermint/basecoin/client/commands/txs" + "github.com/tendermint/basecoin/modules/auth" +) + +//nolint +const ( + FlagMulti = "multi" +) + +// SigWrapper wraps a tx with a signature layer to hold pubkey sigs +type SigWrapper struct{} + +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) { + if !viper.GetBool(FlagMulti) { + res = auth.NewSig(tx).Wrap() + } else { + res = auth.NewMulti(tx).Wrap() + } + return +} + +// Register adds the sequence flags to the cli +func (SigWrapper) Register(fs *pflag.FlagSet) { + fs.Bool(FlagMulti, false, "Prepare the tx for multisig") +} diff --git a/modules/auth/errors.go b/modules/auth/errors.go new file mode 100644 index 00000000000..28352ce2d8c --- /dev/null +++ b/modules/auth/errors.go @@ -0,0 +1,31 @@ +//nolint +package auth + +import ( + "fmt" + + abci "github.com/tendermint/abci/types" + + "github.com/tendermint/basecoin/errors" +) + +var ( + errInvalidSignature = fmt.Errorf("Invalid Signature") //move auth + errTooManySignatures = fmt.Errorf("Too many signatures") //move auth + + unauthorized = abci.CodeType_Unauthorized +) + +func ErrTooManySignatures() errors.TMError { + return errors.WithCode(errTooManySignatures, unauthorized) +} +func IsTooManySignaturesErr(err error) bool { + return errors.IsSameError(errTooManySignatures, err) +} + +func ErrInvalidSignature() errors.TMError { + return errors.WithCode(errInvalidSignature, unauthorized) +} +func IsInvalidSignatureErr(err error) bool { + return errors.IsSameError(errInvalidSignature, err) +} diff --git a/modules/auth/errors_test.go b/modules/auth/errors_test.go new file mode 100644 index 00000000000..887eae1e5c7 --- /dev/null +++ b/modules/auth/errors_test.go @@ -0,0 +1,29 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/tendermint/basecoin/errors" +) + +func TestChecks(t *testing.T) { + // TODO: make sure the Is and Err methods match + assert := assert.New(t) + + cases := []struct { + err error + check func(error) bool + match bool + }{ + // unauthorized includes InvalidSignature, but not visa versa + {ErrInvalidSignature(), IsInvalidSignatureErr, true}, + {ErrInvalidSignature(), errors.IsUnauthorizedErr, true}, + } + + for i, tc := range cases { + match := tc.check(tc.err) + assert.Equal(tc.match, match, "%d", i) + } +} diff --git a/modules/auth/tx.go b/modules/auth/tx.go index 26e2b94b68a..970f321dc55 100644 --- a/modules/auth/tx.go +++ b/modules/auth/tx.go @@ -106,7 +106,7 @@ func (s *OneSig) Sign(pubkey crypto.PubKey, sig crypto.Signature) error { return errors.ErrMissingSignature() } if !s.Empty() { - return errors.ErrTooManySignatures() + return ErrTooManySignatures() } // set the value once we are happy s.Signed = signed @@ -121,7 +121,7 @@ func (s *OneSig) Signers() ([]crypto.PubKey, error) { return nil, errors.ErrMissingSignature() } if !s.Pubkey.VerifyBytes(s.SignBytes(), s.Sig) { - return nil, errors.ErrInvalidSignature() + return nil, ErrInvalidSignature() } return []crypto.PubKey{s.Pubkey}, nil } @@ -194,7 +194,7 @@ func (s *MultiSig) Signers() ([]crypto.PubKey, error) { for i := range s.Sigs { ms := s.Sigs[i] if !ms.Pubkey.VerifyBytes(data, ms.Sig) { - return nil, errors.ErrInvalidSignature() + return nil, ErrInvalidSignature() } keys[i] = ms.Pubkey } diff --git a/modules/auth/tx_test.go b/modules/auth/tx_test.go index 745e4466e6a..03ef42c1244 100644 --- a/modules/auth/tx_test.go +++ b/modules/auth/tx_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/basecoin/stack" crypto "github.com/tendermint/go-crypto" keys "github.com/tendermint/go-crypto/keys" "github.com/tendermint/go-crypto/keys/cryptostore" @@ -14,6 +13,7 @@ import ( wire "github.com/tendermint/go-wire" "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/stack" ) func checkSignBytes(t *testing.T, bytes []byte, expected string) { diff --git a/modules/base/chain.go b/modules/base/chain.go index 65513679de8..9dc704c362f 100644 --- a/modules/base/chain.go +++ b/modules/base/chain.go @@ -2,7 +2,6 @@ package base import ( "github.com/tendermint/basecoin" - "github.com/tendermint/basecoin/errors" "github.com/tendermint/basecoin/stack" "github.com/tendermint/basecoin/state" ) @@ -48,7 +47,7 @@ func (c Chain) checkChainTx(chainID string, height uint64, tx basecoin.Tx) (base // make sure it is a chaintx ctx, ok := tx.Unwrap().(ChainTx) if !ok { - return tx, errors.ErrNoChain() + return tx, ErrNoChain() } // basic validation @@ -59,10 +58,10 @@ func (c Chain) checkChainTx(chainID string, height uint64, tx basecoin.Tx) (base // compare against state if ctx.ChainID != chainID { - return tx, errors.ErrWrongChain(ctx.ChainID) + return tx, ErrWrongChain(ctx.ChainID) } if ctx.ExpiresAt != 0 && ctx.ExpiresAt <= height { - return tx, errors.ErrExpired() + return tx, ErrExpired() } return ctx.Tx, nil } diff --git a/modules/base/commands/wrap.go b/modules/base/commands/wrap.go new file mode 100644 index 00000000000..92e994fd068 --- /dev/null +++ b/modules/base/commands/wrap.go @@ -0,0 +1,39 @@ +package commands + +import ( + "errors" + + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/client/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" + "github.com/tendermint/basecoin/modules/base" +) + +//nolint +const ( + FlagExpires = "expires" +) + +// ChainWrapper wraps a tx with an chain info and optional expiration +type ChainWrapper struct{} + +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) { + expires := viper.GetInt64(FlagExpires) + chain := commands.GetChainID() + if chain == "" { + return res, errors.New("No chain-id provided") + } + res = base.NewChainTx(chain, uint64(expires), tx) + return +} + +// Register adds the sequence flags to the cli +func (ChainWrapper) Register(fs *pflag.FlagSet) { + fs.Uint64(FlagExpires, 0, "Block height at which this tx expires") +} diff --git a/modules/base/errors.go b/modules/base/errors.go new file mode 100644 index 00000000000..8279bc43413 --- /dev/null +++ b/modules/base/errors.go @@ -0,0 +1,37 @@ +//nolint +package base + +import ( + "fmt" + + abci "github.com/tendermint/abci/types" + + "github.com/tendermint/basecoin/errors" +) + +var ( + errNoChain = fmt.Errorf("No chain id provided") //move base + errWrongChain = fmt.Errorf("Wrong chain for tx") //move base + errExpired = fmt.Errorf("Tx expired") //move base + + unauthorized = abci.CodeType_Unauthorized +) + +func ErrNoChain() errors.TMError { + return errors.WithCode(errNoChain, unauthorized) +} +func IsNoChainErr(err error) bool { + return errors.IsSameError(errNoChain, err) +} +func ErrWrongChain(chain string) errors.TMError { + return errors.WithMessage(chain, errWrongChain, unauthorized) +} +func IsWrongChainErr(err error) bool { + return errors.IsSameError(errWrongChain, err) +} +func ErrExpired() errors.TMError { + return errors.WithCode(errExpired, unauthorized) +} +func IsExpiredErr(err error) bool { + return errors.IsSameError(errExpired, err) +} diff --git a/modules/base/errors_test.go b/modules/base/errors_test.go new file mode 100644 index 00000000000..64fda60f847 --- /dev/null +++ b/modules/base/errors_test.go @@ -0,0 +1,45 @@ +package base + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/tendermint/basecoin/errors" +) + +func TestErrorMatches(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + pattern, err error + match bool + }{ + {errWrongChain, ErrWrongChain("hakz"), true}, + } + + for i, tc := range cases { + same := errors.IsSameError(tc.pattern, tc.err) + assert.Equal(tc.match, same, "%d: %#v / %#v", i, tc.pattern, tc.err) + } +} + +func TestChecks(t *testing.T) { + // TODO: make sure the Is and Err methods match + assert := assert.New(t) + + cases := []struct { + err error + check func(error) bool + match bool + }{ + // make sure WrongChain works properly + {ErrWrongChain("fooz"), errors.IsUnauthorizedErr, true}, + {ErrWrongChain("barz"), IsWrongChainErr, true}, + } + + for i, tc := range cases { + match := tc.check(tc.err) + assert.Equal(tc.match, match, "%d", i) + } +} diff --git a/modules/base/tx.go b/modules/base/tx.go index fd66fad8847..d365c1b7b8a 100644 --- a/modules/base/tx.go +++ b/modules/base/tx.go @@ -86,10 +86,10 @@ func (c ChainTx) Wrap() basecoin.Tx { } func (c ChainTx) ValidateBasic() error { if c.ChainID == "" { - return errors.ErrNoChain() + return ErrNoChain() } if !chainPattern.MatchString(c.ChainID) { - return errors.ErrWrongChain(c.ChainID) + return ErrWrongChain(c.ChainID) } if c.Tx.Empty() { return errors.ErrUnknownTxType(c.Tx) diff --git a/modules/base/tx_test.go b/modules/base/tx_test.go index 00bd4ea79ca..1830c469daa 100644 --- a/modules/base/tx_test.go +++ b/modules/base/tx_test.go @@ -7,10 +7,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/basecoin/stack" "github.com/tendermint/go-wire/data" "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/stack" ) func TestEncoding(t *testing.T) { diff --git a/modules/coin/commands/query.go b/modules/coin/commands/query.go new file mode 100644 index 00000000000..7a7d4805ad0 --- /dev/null +++ b/modules/coin/commands/query.go @@ -0,0 +1,42 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + + lc "github.com/tendermint/light-client" + + "github.com/tendermint/basecoin/client/commands" + proofcmd "github.com/tendermint/basecoin/client/commands/proofs" + "github.com/tendermint/basecoin/modules/coin" + "github.com/tendermint/basecoin/stack" +) + +// AccountQueryCmd - command to query an account +var AccountQueryCmd = &cobra.Command{ + Use: "account [address]", + Short: "Get details of an account, with proof", + RunE: commands.RequireInit(accountQueryCmd), +} + +func accountQueryCmd(cmd *cobra.Command, args []string) error { + addr, err := commands.GetOneArg(args, "address") + if err != nil { + return err + } + act, err := commands.ParseActor(addr) + if err != nil { + return err + } + key := stack.PrefixedKey(coin.NameCoin, act.Bytes()) + + acc := coin.Account{} + proof, err := proofcmd.GetAndParseAppProof(key, &acc) + if lc.IsNoDataErr(err) { + return errors.Errorf("Account bytes are empty for address %X ", addr) + } else if err != nil { + return err + } + + return proofcmd.OutputProof(acc, proof.BlockHeight()) +} diff --git a/modules/coin/commands/tx.go b/modules/coin/commands/tx.go new file mode 100644 index 00000000000..cff4a51acb1 --- /dev/null +++ b/modules/coin/commands/tx.go @@ -0,0 +1,79 @@ +package commands + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/client/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" + "github.com/tendermint/basecoin/modules/coin" +) + +// SendTxCmd is CLI command to send tokens between basecoin accounts +var SendTxCmd = &cobra.Command{ + Use: "send", + Short: "send tokens from one account to another", + RunE: commands.RequireInit(sendTxCmd), +} + +//nolint +const ( + FlagTo = "to" + FlagAmount = "amount" + FlagFrom = "from" +) + +func init() { + flags := SendTxCmd.Flags() + flags.String(FlagTo, "", "Destination address for the bits") + flags.String(FlagAmount, "", "Coins to send in the format ,...") + flags.String(FlagFrom, "", "Address sending coins, if not first signer") +} + +// sendTxCmd is an example of how to make a tx +func sendTxCmd(cmd *cobra.Command, args []string) error { + tx, err := readSendTxFlags() + if err != nil { + return err + } + return txcmd.DoTx(tx) +} + +func readSendTxFlags() (tx basecoin.Tx, err error) { + // parse to address + toAddr, err := commands.ParseActor(viper.GetString(FlagTo)) + if err != nil { + return tx, err + } + + fromAddr, err := readFromAddr() + if err != nil { + return tx, err + } + + amountCoins, err := coin.ParseCoins(viper.GetString(FlagAmount)) + if err != nil { + return tx, err + } + + // craft the inputs and outputs + ins := []coin.TxInput{{ + Address: fromAddr, + Coins: amountCoins, + }} + outs := []coin.TxOutput{{ + Address: toAddr, + Coins: amountCoins, + }} + + return coin.NewSendTx(ins, outs), nil +} + +func readFromAddr() (basecoin.Actor, error) { + from := viper.GetString(FlagFrom) + if from == "" { + return txcmd.GetSignerAct(), nil + } + return commands.ParseActor(from) +} diff --git a/modules/coin/errors.go b/modules/coin/errors.go index d245354bff9..49513f43363 100644 --- a/modules/coin/errors.go +++ b/modules/coin/errors.go @@ -5,23 +5,23 @@ import ( "fmt" abci "github.com/tendermint/abci/types" + "github.com/tendermint/basecoin/errors" ) var ( errNoAccount = fmt.Errorf("No such account") - errInsufficientFunds = fmt.Errorf("Insufficient Funds") - errNoInputs = fmt.Errorf("No Input Coins") - errNoOutputs = fmt.Errorf("No Output Coins") - errInvalidAddress = fmt.Errorf("Invalid Address") - errInvalidCoins = fmt.Errorf("Invalid Coins") - errInvalidSequence = fmt.Errorf("Invalid Sequence") -) + errInsufficientFunds = fmt.Errorf("Insufficient funds") + errNoInputs = fmt.Errorf("No input coins") + errNoOutputs = fmt.Errorf("No output coins") + errInvalidAddress = fmt.Errorf("Invalid address") + errInvalidCoins = fmt.Errorf("Invalid coins") + errUnknownKey = fmt.Errorf("Unknown key") -var ( invalidInput = abci.CodeType_BaseInvalidInput invalidOutput = abci.CodeType_BaseInvalidOutput unknownAddress = abci.CodeType_BaseUnknownAddress + unknownRequest = abci.CodeType_UnknownRequest ) // here are some generic handlers to grab classes of errors based on code @@ -80,3 +80,10 @@ func ErrNoOutputs() errors.TMError { func IsNoOutputsErr(err error) bool { return errors.IsSameError(errNoOutputs, err) } + +func ErrUnknownKey(mod string) errors.TMError { + return errors.WithMessage(mod, errUnknownKey, unknownRequest) +} +func IsUnknownKeyErr(err error) bool { + return errors.IsSameError(errUnknownKey, err) +} diff --git a/modules/coin/handler.go b/modules/coin/handler.go index 0e3378db700..ef3f3ae0f67 100644 --- a/modules/coin/handler.go +++ b/modules/coin/handler.go @@ -99,7 +99,7 @@ func (h Handler) SetOption(l log.Logger, store state.KVStore, module, key, value return "Success", nil } - return "", errors.ErrUnknownKey(key) + return "", ErrUnknownKey(key) } func checkTx(ctx basecoin.Context, tx basecoin.Tx) (send SendTx, err error) { diff --git a/modules/coin/helper.go b/modules/coin/helper.go index 21c9ea7825e..4672e85d33a 100644 --- a/modules/coin/helper.go +++ b/modules/coin/helper.go @@ -1,11 +1,11 @@ package coin import ( - "github.com/tendermint/basecoin/modules/auth" crypto "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire/data" "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/modules/auth" ) // AccountWithKey is a helper for tests, that includes and account diff --git a/modules/fee/commands/wrap.go b/modules/fee/commands/wrap.go new file mode 100644 index 00000000000..32fb9848304 --- /dev/null +++ b/modules/fee/commands/wrap.go @@ -0,0 +1,59 @@ +package commands + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin" + "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" +) + +//nolint +const ( + FlagFee = "fee" + FlagPayer = "payer" +) + +// FeeWrapper wraps a tx with an optional fee payment +type FeeWrapper struct{} + +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 +func (FeeWrapper) Wrap(tx basecoin.Tx) (res basecoin.Tx, err error) { + //parse the fee and amounts into coin types + toll, err := coin.ParseCoin(viper.GetString(FlagFee)) + if err != nil { + return res, err + } + // if no fee, do nothing, otherwise wrap it + if toll.IsZero() { + return tx, nil + } + + payer, err := readPayer() + if err != nil { + return res, err + } + + res = fee.NewFee(tx, toll, payer) + return +} + +// Register adds the sequence flags to the cli +func (FeeWrapper) Register(fs *pflag.FlagSet) { + fs.String(FlagFee, "0mycoin", "Coins for the transaction fee of the format ") + fs.String(FlagPayer, "", "Account to pay fee if not current signer (for multisig)") +} + +func readPayer() (basecoin.Actor, error) { + payer := viper.GetString(FlagPayer) + if payer == "" { + return txcmd.GetSignerAct(), nil + } + return commands.ParseActor(payer) +} diff --git a/modules/fee/errors.go b/modules/fee/errors.go index 06cdd10c5c3..5edc8478644 100644 --- a/modules/fee/errors.go +++ b/modules/fee/errors.go @@ -10,19 +10,21 @@ import ( ) var ( - errInsufficientFees = fmt.Errorf("Insufficient Fees") + errInsufficientFees = fmt.Errorf("Insufficient fees") errWrongFeeDenom = fmt.Errorf("Required fee denomination") + + invalidInput = abci.CodeType_BaseInvalidInput ) func ErrInsufficientFees() errors.TMError { - return errors.WithCode(errInsufficientFees, abci.CodeType_BaseInvalidInput) + return errors.WithCode(errInsufficientFees, invalidInput) } func IsInsufficientFeesErr(err error) bool { return errors.IsSameError(errInsufficientFees, err) } func ErrWrongFeeDenom(denom string) errors.TMError { - return errors.WithMessage(denom, errWrongFeeDenom, abci.CodeType_BaseInvalidInput) + return errors.WithMessage(denom, errWrongFeeDenom, invalidInput) } func IsWrongFeeDenomErr(err error) bool { return errors.IsSameError(errWrongFeeDenom, err) diff --git a/modules/nonce/commands/query.go b/modules/nonce/commands/query.go new file mode 100644 index 00000000000..405b0ea94a0 --- /dev/null +++ b/modules/nonce/commands/query.go @@ -0,0 +1,55 @@ +package commands + +import ( + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + lc "github.com/tendermint/light-client" + + "github.com/tendermint/basecoin" + "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" +) + +// NonceQueryCmd - command to query an nonce account +var NonceQueryCmd = &cobra.Command{ + Use: "nonce [address]", + Short: "Get details of a nonce sequence number, with proof", + RunE: commands.RequireInit(nonceQueryCmd), +} + +func nonceQueryCmd(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("Missing required argument [address]") + } + addr := strings.Join(args, ",") + + signers, err := commands.ParseActors(addr) + if err != nil { + return err + } + + seq, proof, err := doNonceQuery(signers) + if err != nil { + return err + } + + return proofcmd.OutputProof(seq, proof.BlockHeight()) +} + +func doNonceQuery(signers []basecoin.Actor) (sequence uint32, proof lc.Proof, err error) { + + key := stack.PrefixedKey(nonce.NameNonce, nonce.GetSeqKey(signers)) + + proof, err = proofcmd.GetAndParseAppProof(key, &sequence) + if lc.IsNoDataErr(err) { + // no data, return sequence 0 + return 0, proof, nil + } + + return +} diff --git a/modules/nonce/commands/wrap.go b/modules/nonce/commands/wrap.go new file mode 100644 index 00000000000..50829466548 --- /dev/null +++ b/modules/nonce/commands/wrap.go @@ -0,0 +1,83 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/client/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" + "github.com/tendermint/basecoin/modules/nonce" +) + +// nolint +const ( + FlagSequence = "sequence" + FlagNonceKey = "nonce-key" +) + +// NonceWrapper wraps a tx with a nonce +type NonceWrapper struct{} + +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, +// as we still only support single sig on the cli +func (NonceWrapper) Wrap(tx basecoin.Tx) (res basecoin.Tx, err error) { + + signers, err := readNonceKey() + if err != nil { + return res, err + } + + seq, err := readSequence(signers) + if err != nil { + return res, err + } + + res = nonce.NewTx(seq, signers, tx) + return +} + +// Register adds the sequence flags to the cli +func (NonceWrapper) Register(fs *pflag.FlagSet) { + fs.Int(FlagSequence, -1, "Sequence number for this transaction") + fs.String(FlagNonceKey, "", "Set of comma-separated addresses for the nonce (for multisig)") +} + +func readNonceKey() ([]basecoin.Actor, error) { + nonce := viper.GetString(FlagNonceKey) + if nonce == "" { + return []basecoin.Actor{txcmd.GetSignerAct()}, nil + } + return commands.ParseActors(nonce) +} + +// read the sequence from the flag or query for it if flag is -1 +func readSequence(signers []basecoin.Actor) (seq uint32, err error) { + //add the nonce tx layer to the tx + seqFlag := viper.GetInt(FlagSequence) + + switch { + case seqFlag > 0: + seq = uint32(seqFlag) + + case seqFlag == -1: + //autocalculation for default sequence + seq, _, err = doNonceQuery(signers) + if err != nil { + return + } + + //increase the sequence by 1! + seq++ + + default: + err = fmt.Errorf("sequence must be either greater than 0, or -1 for autocalculation") + } + + return +} diff --git a/modules/nonce/errors.go b/modules/nonce/errors.go index f88629b4571..832ea8ce2ee 100644 --- a/modules/nonce/errors.go +++ b/modules/nonce/errors.go @@ -11,22 +11,31 @@ import ( var ( errNoNonce = fmt.Errorf("Tx doesn't contain nonce") - errNotMember = fmt.Errorf("nonce contains non-permissioned member") + errNotMember = fmt.Errorf("Nonce contains non-permissioned member") errZeroSequence = fmt.Errorf("Sequence number cannot be zero") + errNoSigners = fmt.Errorf("There are no signers") + errTxEmpty = fmt.Errorf("The provided Tx is empty") unauthorized = abci.CodeType_Unauthorized + badNonce = abci.CodeType_BadNonce + invalidInput = abci.CodeType_BaseInvalidInput ) func ErrBadNonce(got, expected uint32) errors.TMError { - return errors.WithCode(fmt.Errorf("Bad nonce sequence, got %d, expected %d", got, expected), unauthorized) + return errors.WithCode(fmt.Errorf("Bad nonce sequence, got %d, expected %d", got, expected), badNonce) } - func ErrNoNonce() errors.TMError { - return errors.WithCode(errNoNonce, unauthorized) + return errors.WithCode(errNoNonce, badNonce) } func ErrNotMember() errors.TMError { return errors.WithCode(errNotMember, unauthorized) } func ErrZeroSequence() errors.TMError { - return errors.WithCode(errZeroSequence, unauthorized) + return errors.WithCode(errZeroSequence, invalidInput) +} +func ErrNoSigners() errors.TMError { + return errors.WithCode(errNoSigners, invalidInput) +} +func ErrTxEmpty() errors.TMError { + return errors.WithCode(errTxEmpty, invalidInput) } diff --git a/modules/nonce/tx.go b/modules/nonce/tx.go index 7a70878b2d8..6fb5ce7fd2c 100644 --- a/modules/nonce/tx.go +++ b/modules/nonce/tx.go @@ -11,7 +11,6 @@ import ( "sort" "github.com/tendermint/basecoin" - "github.com/tendermint/basecoin/errors" "github.com/tendermint/basecoin/state" ) @@ -50,11 +49,11 @@ func (n Tx) Wrap() basecoin.Tx { func (n Tx) ValidateBasic() error { switch { case n.Tx.Empty(): - return errors.ErrTxEmpty() + return ErrTxEmpty() case n.Sequence == 0: return ErrZeroSequence() case len(n.Signers) == 0: - return errors.ErrNoSigners() + return ErrNoSigners() } return n.Tx.ValidateBasic() } diff --git a/modules/roles/commands/query.go b/modules/roles/commands/query.go new file mode 100644 index 00000000000..d7c97a8e24b --- /dev/null +++ b/modules/roles/commands/query.go @@ -0,0 +1,40 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + + lcmd "github.com/tendermint/basecoin/client/commands" + proofcmd "github.com/tendermint/basecoin/client/commands/proofs" + "github.com/tendermint/basecoin/modules/roles" + "github.com/tendermint/basecoin/stack" +) + +// RoleQueryCmd - command to query a role +var RoleQueryCmd = &cobra.Command{ + Use: "role [name]", + Short: "Get details of a role, with proof", + RunE: lcmd.RequireInit(roleQueryCmd), +} + +func roleQueryCmd(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("Missing required argument [name]") + } else if len(args) > 1 { + return errors.New("Command only supports one name") + } + + role, err := parseRole(args[0]) + if err != nil { + return err + } + + var res roles.Role + key := stack.PrefixedKey(roles.NameRole, role) + proof, err := proofcmd.GetAndParseAppProof(key, &res) + if err != nil { + return err + } + + return proofcmd.OutputProof(res, proof.BlockHeight()) +} diff --git a/modules/roles/commands/tx.go b/modules/roles/commands/tx.go new file mode 100644 index 00000000000..7104131427e --- /dev/null +++ b/modules/roles/commands/tx.go @@ -0,0 +1,65 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/client/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" + "github.com/tendermint/basecoin/modules/roles" +) + +// CreateRoleTxCmd is CLI command to send tokens between basecoin accounts +var CreateRoleTxCmd = &cobra.Command{ + Use: "create-role", + Short: "Create a new role", + RunE: commands.RequireInit(createRoleTxCmd), +} + +//nolint +const ( + FlagRole = "role" + FlagMembers = "members" + FlagMinSigs = "min-sigs" +) + +func init() { + flags := CreateRoleTxCmd.Flags() + flags.String(FlagRole, "", "Name of the role to create") + flags.String(FlagMembers, "", "Set of comma-separated addresses for this role") + flags.Int(FlagMinSigs, 0, "Minimum number of signatures needed to assume this role") +} + +// createRoleTxCmd is an example of how to make a tx +func createRoleTxCmd(cmd *cobra.Command, args []string) error { + tx, err := readCreateRoleTxFlags() + if err != nil { + return err + } + return txcmd.DoTx(tx) +} + +func readCreateRoleTxFlags() (tx basecoin.Tx, err error) { + role, err := parseRole(viper.GetString(FlagRole)) + if err != nil { + return tx, err + } + + sigs := viper.GetInt(FlagMinSigs) + if sigs < 1 { + return tx, errors.Errorf("--%s must be at least 1", FlagMinSigs) + } + + signers, err := commands.ParseActors(viper.GetString(FlagMembers)) + if err != nil { + return tx, err + } + if len(signers) == 0 { + return tx, errors.New("must specify at least one member") + } + + tx = roles.NewCreateRoleTx(role, uint32(sigs), signers) + return tx, nil +} diff --git a/modules/roles/commands/wrap.go b/modules/roles/commands/wrap.go new file mode 100644 index 00000000000..48d85a2e779 --- /dev/null +++ b/modules/roles/commands/wrap.go @@ -0,0 +1,48 @@ +package commands + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin" + txcmd "github.com/tendermint/basecoin/client/commands/txs" + "github.com/tendermint/basecoin/modules/roles" +) + +// nolint +const ( + FlagAssumeRole = "assume-role" +) + +// RoleWrapper wraps a tx with 0, 1, or more roles +type RoleWrapper struct{} + +var _ txcmd.Wrapper = RoleWrapper{} + +// Wrap grabs the sequence number from the flag and wraps +// the tx with this nonce. Grabs the permission from the signer, +// as we still only support single sig on the cli +func (RoleWrapper) Wrap(tx basecoin.Tx) (basecoin.Tx, error) { + assume := viper.GetStringSlice(FlagAssumeRole) + + // we wrap from inside-out, so we must wrap them in the reverse order, + // so they are applied in the order the user intended + for i := len(assume) - 1; i >= 0; i-- { + r, err := parseRole(assume[i]) + if err != nil { + return tx, err + } + tx = roles.NewAssumeRoleTx(r, tx) + } + return tx, nil +} + +// Register adds the sequence flags to the cli +func (RoleWrapper) Register(fs *pflag.FlagSet) { + fs.StringSlice(FlagAssumeRole, nil, "Roles to assume (can use multiple times)") +} + +// parse role turns the string->byte... todo: support hex? +func parseRole(role string) ([]byte, error) { + return []byte(role), nil +} diff --git a/modules/roles/error.go b/modules/roles/error.go index 1fb8333a278..62ff77226c2 100644 --- a/modules/roles/error.go +++ b/modules/roles/error.go @@ -5,6 +5,7 @@ import ( "fmt" abci "github.com/tendermint/abci/types" + "github.com/tendermint/basecoin/errors" ) @@ -16,54 +17,56 @@ var ( errNoMembers = fmt.Errorf("No members specified") errTooManyMembers = fmt.Errorf("Too many members specified") errNotEnoughMembers = fmt.Errorf("Not enough members specified") + + unauthorized = abci.CodeType_Unauthorized ) // TODO: codegen? // ex: err-gen NoRole,"No such role",CodeType_Unauthorized func ErrNoRole() errors.TMError { - return errors.WithCode(errNoRole, abci.CodeType_Unauthorized) + return errors.WithCode(errNoRole, unauthorized) } func IsNoRoleErr(err error) bool { return errors.IsSameError(errNoRole, err) } func ErrRoleExists() errors.TMError { - return errors.WithCode(errRoleExists, abci.CodeType_Unauthorized) + return errors.WithCode(errRoleExists, unauthorized) } func IsRoleExistsErr(err error) bool { return errors.IsSameError(errRoleExists, err) } func ErrNotMember() errors.TMError { - return errors.WithCode(errNotMember, abci.CodeType_Unauthorized) + return errors.WithCode(errNotMember, unauthorized) } func IsNotMemberErr(err error) bool { return errors.IsSameError(errNotMember, err) } func ErrInsufficientSigs() errors.TMError { - return errors.WithCode(errInsufficientSigs, abci.CodeType_Unauthorized) + return errors.WithCode(errInsufficientSigs, unauthorized) } func IsInsufficientSigsErr(err error) bool { return errors.IsSameError(errInsufficientSigs, err) } func ErrNoMembers() errors.TMError { - return errors.WithCode(errNoMembers, abci.CodeType_Unauthorized) + return errors.WithCode(errNoMembers, unauthorized) } func IsNoMembersErr(err error) bool { return errors.IsSameError(errNoMembers, err) } func ErrTooManyMembers() errors.TMError { - return errors.WithCode(errTooManyMembers, abci.CodeType_Unauthorized) + return errors.WithCode(errTooManyMembers, unauthorized) } func IsTooManyMembersErr(err error) bool { return errors.IsSameError(errTooManyMembers, err) } func ErrNotEnoughMembers() errors.TMError { - return errors.WithCode(errNotEnoughMembers, abci.CodeType_Unauthorized) + return errors.WithCode(errNotEnoughMembers, unauthorized) } func IsNotEnoughMembersErr(err error) bool { return errors.IsSameError(errNotEnoughMembers, err) diff --git a/modules/roles/handler_test.go b/modules/roles/handler_test.go index e5c744bbc8a..6d1c5dfa616 100644 --- a/modules/roles/handler_test.go +++ b/modules/roles/handler_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/tendermint/basecoin" "github.com/tendermint/basecoin/modules/roles" "github.com/tendermint/basecoin/stack" diff --git a/modules/roles/store.go b/modules/roles/store.go index c29d93f0f8b..eb114ff6f23 100644 --- a/modules/roles/store.go +++ b/modules/roles/store.go @@ -3,10 +3,11 @@ package roles import ( "fmt" + wire "github.com/tendermint/go-wire" + "github.com/tendermint/basecoin" "github.com/tendermint/basecoin/errors" "github.com/tendermint/basecoin/state" - wire "github.com/tendermint/go-wire" ) // NewPerm creates a role permission with the given label diff --git a/stack/state_space_test.go b/stack/state_space_test.go index 2722ad6a3c0..071b326600f 100644 --- a/stack/state_space_test.go +++ b/stack/state_space_test.go @@ -6,11 +6,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tendermint/go-wire/data" "github.com/tendermint/tmlibs/log" "github.com/tendermint/basecoin" "github.com/tendermint/basecoin/state" - "github.com/tendermint/go-wire/data" ) // writerMid is a middleware that writes the given bytes on CheckTx and DeliverTx diff --git a/tests/cli/basictx.sh b/tests/cli/basictx.sh index 009e1ddc022..d6ab7ea9ea2 100755 --- a/tests/cli/basictx.sh +++ b/tests/cli/basictx.sh @@ -21,20 +21,20 @@ test00GetAccount() { SENDER=$(getAddr $RICH) RECV=$(getAddr $POOR) - assertFalse "requires arg" "${CLIENT_EXE} query account" + assertFalse "line=${LINENO}, requires arg" "${CLIENT_EXE} query account" checkAccount $SENDER "9007199254740992" ACCT2=$(${CLIENT_EXE} query account $RECV 2>/dev/null) - assertFalse "has no genesis account" $? + assertFalse "line=${LINENO}, has no genesis account" $? } test01SendTx() { SENDER=$(getAddr $RICH) RECV=$(getAddr $POOR) - assertFalse "missing dest" "${CLIENT_EXE} tx send --amount=992mycoin --sequence=1" - assertFalse "bad password" "echo foo | ${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 --to=$RECV --name=$RICH" + assertFalse "line=${LINENO}, missing dest" "${CLIENT_EXE} tx send --amount=992mycoin --sequence=1" + assertFalse "line=${LINENO}, bad password" "echo foo | ${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 --to=$RECV --name=$RICH" TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 --to=$RECV --name=$RICH) txSucceeded $? "$TX" "$RECV" HASH=$(echo $TX | jq .hash | tr -d \") @@ -53,7 +53,8 @@ test02SendTxWithFee() { SENDER=$(getAddr $RICH) RECV=$(getAddr $POOR) - TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=90mycoin --fee=10mycoin --sequence=2 --to=$RECV --name=$RICH) + # Test to see if the auto-sequencing works, the sequence here should be calculated to be 2 + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=90mycoin --fee=10mycoin --to=$RECV --name=$RICH) txSucceeded $? "$TX" "$RECV" HASH=$(echo $TX | jq .hash | tr -d \") TX_HEIGHT=$(echo $TX | jq .height) @@ -67,7 +68,7 @@ test02SendTxWithFee() { # assert replay protection TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=90mycoin --fee=10mycoin --sequence=2 --to=$RECV --name=$RICH 2>/dev/null) - assertFalse "replay: $TX" $? + assertFalse "line=${LINENO}, replay: $TX" $? checkAccount $SENDER "9007199254739900" checkAccount $RECV "1082" @@ -76,7 +77,7 @@ test02SendTxWithFee() { if [ -n "$DEBUG" ]; then echo $NONCE; echo; fi # TODO: note that cobra returns error code 0 on parse failure, # so currently this check passes even if there is no nonce query command - if assertTrue "no nonce query" $?; then + if assertTrue "line=${LINENO}, no nonce query" $?; then assertEquals "line=${LINENO}, proper nonce" "2" $(echo $NONCE | jq .data) fi } diff --git a/tests/cli/common.sh b/tests/cli/common.sh index b195184b437..f50ef6b9dcf 100644 --- a/tests/cli/common.sh +++ b/tests/cli/common.sh @@ -138,10 +138,27 @@ checkAccount() { return $? } +# XXX Ex Usage: checkRole $ROLE $SIGS $NUM_SIGNERS +# Desc: Ensures this named role exists, and has the number of members and required signatures as above +checkRole() { + # make sure sender goes down + ROLE=$(${CLIENT_EXE} query role $1) + if ! assertTrue "line=${LINENO}, role must exist" $?; then + return 1 + fi + + if [ -n "$DEBUG" ]; then echo $ROLE; echo; fi + assertEquals "line=${LINENO}, proper sigs" "$2" $(echo $ROLE | jq .data.min_sigs) + assertEquals "line=${LINENO}, proper app" '"sigs"' $(echo $ROLE | jq '.data.signers[0].app' ) + assertEquals "line=${LINENO}, proper signers" "$3" $(echo $ROLE | jq '.data.signers | length') + return $? +} + + # XXX Ex Usage: txSucceeded $? "$TX" "$RECIEVER" # Desc: Must be called right after the `tx` command, makes sure it got a success response txSucceeded() { - if (assertTrue "sent tx ($3): $2" $1); then + if (assertTrue "line=${LINENO}, sent tx ($3): $2" $1); then TX=$2 assertEquals "line=${LINENO}, good check ($3): $TX" "0" $(echo $TX | jq .check_tx.code) assertEquals "line=${LINENO}, good deliver ($3): $TX" "0" $(echo $TX | jq .deliver_tx.code) @@ -171,13 +188,43 @@ checkSendTx() { return $? } +# XXX Ex Usage: toHex "my-name" +# converts the string into the hex representation of the bytes +toHex() { + echo -n $1 | od -A n -t x1 | sed 's/ //g' | tr 'a-f' 'A-F' +} + +# XXX Ex Usage: checkRoleTx $HASH $HEIGHT $NAME $NUM_SIGNERS +# Desc: This looks up the tx by hash, and makes sure the height and type match +# and that the it refers to the proper role +checkRoleTx() { + TX=$(${CLIENT_EXE} query tx $1) + assertTrue "line=${LINENO}, found tx" $? + if [ -n "$DEBUG" ]; then echo $TX; echo; fi + + + assertEquals "line=${LINENO}, proper height" $2 $(echo $TX | jq .height) + assertEquals "line=${LINENO}, type=sigs/one" '"sigs/one"' $(echo $TX | jq .data.type) + CTX=$(echo $TX | jq .data.data.tx) + assertEquals "line=${LINENO}, type=chain/tx" '"chain/tx"' $(echo $CTX | jq .type) + NTX=$(echo $CTX | jq .data.tx) + assertEquals "line=${LINENO}, type=nonce" '"nonce"' $(echo $NTX | jq .type) + RTX=$(echo $NTX | jq .data.tx) + assertEquals "line=${LINENO}, type=role/create" '"role/create"' $(echo $RTX | jq .type) + HEXNAME=$(toHex $3) + assertEquals "line=${LINENO}, proper name" "\"$HEXNAME\"" $(echo $RTX | jq .data.role) + assertEquals "line=${LINENO}, proper num signers" "$4" $(echo $RTX | jq '.data.signers | length') + return $? +} + + # XXX Ex Usage: checkSendFeeTx $HASH $HEIGHT $SENDER $AMOUNT $FEE # Desc: This is like checkSendTx, but asserts a feetx wrapper with $FEE value. # This looks up the tx by hash, and makes sure the height and type match # and that the first input was from this sender for this amount checkSendFeeTx() { TX=$(${CLIENT_EXE} query tx $1) - assertTrue "found tx" $? + assertTrue "line=${LINENO}, found tx" $? if [ -n "$DEBUG" ]; then echo $TX; echo; fi assertEquals "line=${LINENO}, proper height" $2 $(echo $TX | jq .height) diff --git a/tests/cli/counter.sh b/tests/cli/counter.sh index f21c9066d0d..f57827a9637 100755 --- a/tests/cli/counter.sh +++ b/tests/cli/counter.sh @@ -33,9 +33,10 @@ test01SendTx() { SENDER=$(getAddr $RICH) RECV=$(getAddr $POOR) - assertFalse "Line=${LINENO}, missing dest" "${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 2>/dev/null" - assertFalse "Line=${LINENO}, bad password" "echo foo | ${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 --to=$RECV --name=$RICH 2>/dev/null" - TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 --to=$RECV --name=$RICH) + # sequence should work well for first time also + assertFalse "Line=${LINENO}, missing dest" "${CLIENT_EXE} tx send --amount=992mycoin 2>/dev/null" + assertFalse "Line=${LINENO}, bad password" "echo foo | ${CLIENT_EXE} tx send --amount=992mycoin --to=$RECV --name=$RICH 2>/dev/null" + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=992mycoin --to=$RECV --name=$RICH) txSucceeded $? "$TX" "$RECV" HASH=$(echo $TX | jq .hash | tr -d \") TX_HEIGHT=$(echo $TX | jq .height) @@ -104,7 +105,7 @@ test03AddCount() { # make sure we cannot replay the counter, no state change TX=$(echo qwertyuiop | ${CLIENT_EXE} tx counter --countfee=10mycoin --sequence=2 --name=${RICH} --valid 2>/dev/null) - assertFalse "replay: $TX" $? + assertFalse "line=${LINENO}, replay: $TX" $? checkCounter "2" "17" checkAccount $SENDER "9007199254739979" } diff --git a/tests/cli/ibc.sh b/tests/cli/ibc.sh index aea38fba555..36f380dedeb 100755 --- a/tests/cli/ibc.sh +++ b/tests/cli/ibc.sh @@ -66,21 +66,21 @@ test00GetAccount() { RECV_1=$(BC_HOME=${CLIENT_1} getAddr $POOR) export BC_HOME=${CLIENT_1} - assertFalse "requires arg" "${CLIENT_EXE} query account 2>/dev/null" - assertFalse "has no genesis account" "${CLIENT_EXE} query account $RECV_1 2>/dev/null" + assertFalse "line=${LINENO}, requires arg" "${CLIENT_EXE} query account 2>/dev/null" + assertFalse "line=${LINENO}, has no genesis account" "${CLIENT_EXE} query account $RECV_1 2>/dev/null" checkAccount $SENDER_1 "0" "9007199254740992" export BC_HOME=${CLIENT_2} SENDER_2=$(getAddr $RICH) RECV_2=$(getAddr $POOR) - assertFalse "requires arg" "${CLIENT_EXE} query account 2>/dev/null" - assertFalse "has no genesis account" "${CLIENT_EXE} query account $RECV_2 2>/dev/null" + assertFalse "line=${LINENO}, requires arg" "${CLIENT_EXE} query account 2>/dev/null" + assertFalse "line=${LINENO}, has no genesis account" "${CLIENT_EXE} query account $RECV_2 2>/dev/null" checkAccount $SENDER_2 "0" "9007199254740992" # Make sure that they have different addresses on both chains (they are random keys) - assertNotEquals "sender keys must be different" "$SENDER_1" "$SENDER_2" - assertNotEquals "recipient keys must be different" "$RECV_1" "$RECV_2" + assertNotEquals "line=${LINENO}, sender keys must be different" "$SENDER_1" "$SENDER_2" + assertNotEquals "line=${LINENO}, recipient keys must be different" "$RECV_1" "$RECV_2" } test01SendIBCTx() { @@ -105,7 +105,7 @@ test01SendIBCTx() { # Make sure nothing arrived - yet waitForBlock ${PORT_1} - assertFalse "no relay running" "BC_HOME=${CLIENT_2} ${CLIENT_EXE} query account $RECV" + assertFalse "line=${LINENO}, no relay running" "BC_HOME=${CLIENT_2} ${CLIENT_EXE} query account $RECV" # Start the relay and wait a few blocks... # (already sent a tx on chain1, so use higher sequence) diff --git a/tests/cli/init.sh b/tests/cli/init.sh new file mode 100755 index 00000000000..9de8057c453 --- /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 "line=${LINENO}" $?; 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 "line=${LINENO}" "ls ${BCHOME} 2>/dev/null >&2" + + echo y | ${CLIENT_EXE} init --node=tcp://localhost:46657 --chain-id="${CHAIN_ID}" > /dev/null + assertTrue "line=${LINENO}, initialized light-client" $? + checkDir $BCHOME 3 +} + +test02badInit() { + export BCHOME=${BASE}/client-02 + assertFalse "line=${LINENO}" "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 "line=${LINENO}, 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 "line=${LINENO}, 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 "line=${LINENO}, invalid init" $? + checkDir $BCHOME 0 +} + +test03noDoubleInit() { + export BCHOME=${BASE}/client-03 + assertFalse "line=${LINENO}" "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 "line=${LINENO}, 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 "line=${LINENO}, 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 "line=${LINENO}, re-initialized light-client" $? + checkDir $BCHOME 3 +} + +test04acceptGenesisFile() { + export BCHOME=${BASE}/client-04 + assertFalse "line=${LINENO}" "ls ${BCHOME} 2>/dev/null >&2" + + # init properly + ${CLIENT_EXE} init --node=tcp://localhost:46657 --genesis=${GENESIS_FILE} > /dev/null 2>&1 + assertTrue "line=${LINENO}, initialized light-client" $? + checkDir $BCHOME 3 +} + +# XXX Ex: checkDir $DIR $FILES +# Makes sure directory exists and has the given number of files +checkDir() { + assertTrue "line=${LINENO}" "ls ${1} 2>/dev/null >&2" + assertEquals "line=${LINENO}, 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 00000000000..89e1ee53851 --- /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 "line ${LINENO}" $? +} + +newKey(){ + assertNotNull "keyname required" "$1" + KEYPASS=${2:-qwertyuiop} + echo $KEYPASS | ${CLIENT_EXE} keys new $1 >/dev/null 2>&1 + assertTrue "line ${LINENO}, created $1" $? +} + +testMakeKeys() { + USER=demouser + assertFalse "line ${LINENO}, already user $USER" "${CLIENT_EXE} keys get $USER" + newKey $USER + assertTrue "line ${LINENO}, 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/roles.sh b/tests/cli/roles.sh new file mode 100755 index 00000000000..5457090b92e --- /dev/null +++ b/tests/cli/roles.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# These global variables are required for common.sh +SERVER_EXE=basecoin +CLIENT_EXE=basecli +ACCOUNTS=(jae ethan bucky rigel igor) +RICH=${ACCOUNTS[0]} +POOR=${ACCOUNTS[4]} +DUDE=${ACCOUNTS[2]} + +oneTimeSetUp() { + if ! quickSetup .basecoin_test_roles roles-chain; then + exit 1; + fi +} + +oneTimeTearDown() { + quickTearDown +} + +test01SetupRole() { + ONE=$(getAddr $RICH) + TWO=$(getAddr $POOR) + THREE=$(getAddr $DUDE) + MEMBERS=${ONE},${TWO},${THREE} + + SIGS=2 + + assertFalse "line=${LINENO}, missing min-sigs" "echo qwertyuiop | ${CLIENT_EXE} tx create-role --role=bank --members=${MEMBERS} --sequence=1 --name=$RICH" + assertFalse "line=${LINENO}, missing members" "echo qwertyuiop | ${CLIENT_EXE} tx create-role --role=bank --min-sigs=2 --sequence=1 --name=$RICH" + assertFalse "line=${LINENO}, missing role" "echo qwertyuiop | ${CLIENT_EXE} tx create-role --min-sigs=2 --members=${MEMBERS} --sequence=1 --name=$RICH" + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx create-role --role=bank --min-sigs=$SIGS --members=${MEMBERS} --sequence=1 --name=$RICH) + txSucceeded $? "$TX" "bank" + HASH=$(echo $TX | jq .hash | tr -d \") + TX_HEIGHT=$(echo $TX | jq .height) + + checkRole bank $SIGS 3 + + # Make sure tx is indexed + checkRoleTx $HASH $TX_HEIGHT "bank" 3 +} + +test02SendTxToRole() { + SENDER=$(getAddr $RICH) + RECV=role:$(toHex bank) + + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --fee=90mycoin --amount=10000mycoin --to=$RECV --sequence=2 --name=$RICH) + txSucceeded $? "$TX" "bank" + HASH=$(echo $TX | jq .hash | tr -d \") + TX_HEIGHT=$(echo $TX | jq .height) + + # reduce by 10090 + checkAccount $SENDER "9007199254730902" + checkAccount $RECV "10000" + + checkSendFeeTx $HASH $TX_HEIGHT $SENDER "10000" "90" +} + +test03SendMultiFromRole() { + ONE=$(getAddr $RICH) + TWO=$(getAddr $POOR) + THREE=$(getAddr $DUDE) + BANK=role:$(toHex bank) + + # no money to start mr. poor... + assertFalse "line=${LINENO}, has no money yet" "${CLIENT_EXE} query account $TWO 2>/dev/null" + + # let's try to send money from the role directly without multisig + FAIL=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=6000mycoin --from=$BANK --to=$TWO --sequence=1 --name=$POOR 2>/dev/null) + assertFalse "need to assume role" $? + FAIL=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=6000mycoin --from=$BANK --to=$TWO --sequence=1 --assume-role=bank --name=$POOR 2>/dev/null) + assertFalse "need two signatures" $? + + # okay, begin a multisig transaction mr. poor... + TX_FILE=$BASE_DIR/tx.json + echo qwertyuiop | ${CLIENT_EXE} tx send --amount=6000mycoin --from=$BANK --to=$TWO --sequence=1 --assume-role=bank --name=$POOR --multi --prepare=$TX_FILE + assertTrue "line=${LINENO}, successfully prepare tx" $? + # and get some dude to sign it + FAIL=$(echo qwertyuiop | ${CLIENT_EXE} tx --in=$TX_FILE --name=$POOR 2>/dev/null) + assertFalse "line=${LINENO}, double signing doesn't get bank" $? + # and get some dude to sign it for the full access + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx --in=$TX_FILE --name=$DUDE) + txSucceeded $? "$TX" "multi-bank" + + checkAccount $TWO "6000" + checkAccount $BANK "4000" +} + + +# Load common then run these tests with shunit2! +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #get this files directory +. $DIR/common.sh +. $DIR/shunit2 diff --git a/tests/cli/rpc.sh b/tests/cli/rpc.sh new file mode 100755 index 00000000000..1026ee89029 --- /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 "line=${LINENO}" $?; 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 "line=${LINENO}, 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 "line=${LINENO}, get genesis" "$?" + MYCHAIN=$(echo ${GENESIS} | jq .genesis.chain_id | tr -d \") + assertEquals "line=${LINENO}, genesis chain matches" "${CHAIN_ID}" "${MYCHAIN}" + + STATUS=$(${CLIENT_EXE} rpc status) + assertTrue "line=${LINENO}, get status" "$?" + SHEIGHT=$(echo ${STATUS} | jq .latest_block_height) + assertTrue "line=${LINENO}, parsed status" "$?" + assertNotNull "line=${LINENO}, has a height" "${SHEIGHT}" + + VALS=$(${CLIENT_EXE} rpc validators) + assertTrue "line=${LINENO}, get validators" "$?" + VHEIGHT=$(echo ${VALS} | jq .block_height) + assertTrue "line=${LINENO}, parsed validators" "$?" + assertTrue "line=${LINENO}, sensible heights: $SHEIGHT / $VHEIGHT" "test $VHEIGHT -ge $SHEIGHT" + VCNT=$(echo ${VALS} | jq '.validators | length') + assertEquals "line=${LINENO}, one validator" "1" "$VCNT" + + INFO=$(${CLIENT_EXE} rpc info) + assertTrue "line=${LINENO}, get info" "$?" + DATA=$(echo $INFO | jq .response.data) + assertEquals "line=${LINENO}, basecoin info" '"Basecoin v0.6.1"' "$DATA" +} + +test02GetSecure() { + HEIGHT=$(${CLIENT_EXE} rpc status | jq .latest_block_height) + assertTrue "line=${LINENO}, get status" "$?" + + # check block produces something reasonable + assertFalse "line=${LINENO}, missing height" "${CLIENT_EXE} rpc block" + BLOCK=$(${CLIENT_EXE} rpc block --height=$HEIGHT) + assertTrue "line=${LINENO}, get block" "$?" + MHEIGHT=$(echo $BLOCK | jq .block_meta.header.height) + assertEquals "line=${LINENO}, meta height" "${HEIGHT}" "${MHEIGHT}" + BHEIGHT=$(echo $BLOCK | jq .block.header.height) + assertEquals "line=${LINENO}, meta height" "${HEIGHT}" "${BHEIGHT}" + + # check commit produces something reasonable + assertFalse "line=${LINENO}, missing height" "${CLIENT_EXE} rpc commit" + let "CHEIGHT = $HEIGHT - 1" + COMMIT=$(${CLIENT_EXE} rpc commit --height=$CHEIGHT) + assertTrue "line=${LINENO}, get commit" "$?" + HHEIGHT=$(echo $COMMIT | jq .header.height) + assertEquals "line=${LINENO}, commit height" "${CHEIGHT}" "${HHEIGHT}" + assertEquals "line=${LINENO}, canonical" "true" $(echo $COMMIT | jq .canonical) + BSIG=$(echo $BLOCK | jq .block.last_commit) + CSIG=$(echo $COMMIT | jq .commit) + assertEquals "line=${LINENO}, 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 "line=${LINENO}, get headers" "$?" + assertEquals "line=${LINENO}, proper height" "$HEIGHT" $(echo $HEADERS | jq '.last_height') + assertEquals "line=${LINENO}, 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 "line=${LINENO}, commit and header" "$CHEAD" "$HHEAD" +} + +test03Waiting() { + START=$(${CLIENT_EXE} rpc status | jq .latest_block_height) + assertTrue "line=${LINENO}, get status" "$?" + + let "NEXT = $START + 5" + assertFalse "line=${LINENO}, no args" "${CLIENT_EXE} rpc wait" + assertFalse "line=${LINENO}, too long" "${CLIENT_EXE} rpc wait --height=1234" + assertTrue "line=${LINENO}, normal wait" "${CLIENT_EXE} rpc wait --height=$NEXT" + + STEP=$(${CLIENT_EXE} rpc status | jq .latest_block_height) + assertEquals "line=${LINENO}, wait until height" "$NEXT" "$STEP" + + let "NEXT = $STEP + 3" + assertTrue "line=${LINENO}, ${CLIENT_EXE} rpc wait --delta=3" + STEP=$(${CLIENT_EXE} rpc status | jq .latest_block_height) + assertEquals "line=${LINENO}, 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