Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R4R: Allow --from to be a name or an address #2073

Merged
merged 16 commits into from
Sep 25, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ BREAKING CHANGES
* [cli] \#2061 changed proposalID in governance REST endpoints to proposal-id
* [cli] \#2014 `gaiacli advanced` no longer exists - to access `ibc`, `rest-server`, and `validator-set` commands use `gaiacli ibc`, `gaiacli rest-server`, and `gaiacli tendermint`, respectively
* [makefile] `get_vendor_deps` no longer updates lock file it just updates vendor directory. Use `update_vendor_deps` to update the lock file. [#2152](https://github.com/cosmos/cosmos-sdk/pull/2152)
* [cli] \#2073 --from can now be either an address or a key name

* Gaia
* Make the transient store key use a distinct store key. [#2013](https://github.com/cosmos/cosmos-sdk/pull/2013)
Expand Down
64 changes: 33 additions & 31 deletions client/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,31 @@ import (
"github.com/spf13/viper"

rpcclient "github.com/tendermint/tendermint/rpc/client"
"github.com/cosmos/cosmos-sdk/types"
)

const ctxAccStoreName = "acc"

// CLIContext implements a typical CLI context created in SDK modules for
// transaction handling and queries.
type CLIContext struct {
Codec *wire.Codec
AccDecoder auth.AccountDecoder
Client rpcclient.Client
Logger io.Writer
Height int64
Gas int64
GasAdjustment float64
NodeURI string
FromAddressName string
AccountStore string
TrustNode bool
UseLedger bool
Async bool
JSON bool
PrintResponse bool
Codec *wire.Codec
AccDecoder auth.AccountDecoder
Client rpcclient.Client
Logger io.Writer
Height int64
Gas int64
GasAdjustment float64
NodeURI string
From string
AccountStore string
TrustNode bool
UseLedger bool
Async bool
JSON bool
PrintResponse bool
fromAddress types.AccAddress
fromName string
}

// NewCLIContext returns a new initialized CLIContext with parameters from the
Expand All @@ -45,18 +48,18 @@ func NewCLIContext() CLIContext {
}

return CLIContext{
Client: rpc,
NodeURI: nodeURI,
AccountStore: ctxAccStoreName,
FromAddressName: viper.GetString(client.FlagFrom),
Height: viper.GetInt64(client.FlagHeight),
Gas: viper.GetInt64(client.FlagGas),
GasAdjustment: viper.GetFloat64(client.FlagGasAdjustment),
TrustNode: viper.GetBool(client.FlagTrustNode),
UseLedger: viper.GetBool(client.FlagUseLedger),
Async: viper.GetBool(client.FlagAsync),
JSON: viper.GetBool(client.FlagJson),
PrintResponse: viper.GetBool(client.FlagPrintResponse),
Client: rpc,
NodeURI: nodeURI,
AccountStore: ctxAccStoreName,
From: viper.GetString(client.FlagFrom),
Height: viper.GetInt64(client.FlagHeight),
Gas: viper.GetInt64(client.FlagGas),
GasAdjustment: viper.GetFloat64(client.FlagGasAdjustment),
TrustNode: viper.GetBool(client.FlagTrustNode),
UseLedger: viper.GetBool(client.FlagUseLedger),
Async: viper.GetBool(client.FlagAsync),
JSON: viper.GetBool(client.FlagJson),
PrintResponse: viper.GetBool(client.FlagPrintResponse),
}
}

Expand Down Expand Up @@ -85,10 +88,9 @@ func (ctx CLIContext) WithAccountStore(accountStore string) CLIContext {
return ctx
}

// WithFromAddressName returns a copy of the context with an updated from
// address.
func (ctx CLIContext) WithFromAddressName(addrName string) CLIContext {
ctx.FromAddressName = addrName
// WithFrom returns a copy of the context with an updated from address or name.
func (ctx CLIContext) WithFrom(from string) CLIContext {
ctx.From = from
return ctx
}

Expand Down
48 changes: 40 additions & 8 deletions client/context/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
cmn "github.com/tendermint/tendermint/libs/common"
rpcclient "github.com/tendermint/tendermint/rpc/client"
ctypes "github.com/tendermint/tendermint/rpc/core/types"
cskeys "github.com/cosmos/cosmos-sdk/crypto/keys"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting is off (use gofmt)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting is still off here @mslipper

)

// GetNode returns an RPC client. If the context's client is not defined, an
Expand Down Expand Up @@ -76,22 +77,53 @@ func (ctx CLIContext) GetAccount(address []byte) (auth.Account, error) {
}

// GetFromAddress returns the from address from the context's name.
func (ctx CLIContext) GetFromAddress() (from sdk.AccAddress, err error) {
if ctx.FromAddressName == "" {
return nil, errors.Errorf("must provide a from address name")
func (ctx CLIContext) GetFromAddress() (sdk.AccAddress, error) {
if ctx.fromAddress == nil {
if err := ctx.populateFromFields(); err != nil {
return nil, err
}
}

return ctx.fromAddress, nil
}

// GetFromname returns the key name for the current context.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetFromName*

func (ctx CLIContext) GetFromName() (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing godoc

if ctx.fromName == "" {
if err := ctx.populateFromFields(); err != nil {
return "", err
}
}

return ctx.fromName, nil
}

func (ctx CLIContext) populateFromFields() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this logic is correct? I have not been able to get this to work (gaiacli send --from <addr1> --to <addr2> ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep - note that you'll have to create a new key for the correct data to appear in the database.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear: the way I've written it, an additional key (as in key-value store) is written to LevelDB to look up keys (as in private keys) by address. If that KV-store key doesn't exist, you won't be able to pass a public key in --from.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mslipper I created a fresh local testnet, added all of my node's keys to a single node's key DB and still could not get this to work. I've tried with both recovered keys and with newly created keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you able to get it working @alexanderbez ? If you could post the command you're using and the error you get I can debug.

if ctx.From == "" {
return errors.Errorf("must provide a from address or name")
}

keybase, err := keys.GetKeyBase()
if err != nil {
return nil, err
return err
}

info, err := keybase.Get(ctx.FromAddressName)
if err != nil {
return nil, errors.Errorf("no key for: %s", ctx.FromAddressName)
var info cskeys.Info
if addr, err := sdk.AccAddressFromBech32(ctx.From); err == nil {
info, err = keybase.GetByAddress(addr)
if err != nil {
return err
}
} else {
info, err = keybase.Get(ctx.From)
if err != nil {
return err
}
}

return sdk.AccAddress(info.GetPubKey().Address()), nil
ctx.fromAddress = (sdk.AccAddress)(info.GetPubKey().Address())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this equivalent to: sdk.AccAddress(info.GetPubKey().Address().Bytes())?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Both Address() and Bytes() return effectively a byte slice.

ctx.fromName = info.GetName()
return nil
}

// GetAccountNumber returns the next account number for the given account
Expand Down
2 changes: 1 addition & 1 deletion client/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func GetCommands(cmds ...*cobra.Command) []*cobra.Command {
// PostCommands adds common flags for commands to post tx
func PostCommands(cmds ...*cobra.Command) []*cobra.Command {
for _, c := range cmds {
c.Flags().String(FlagFrom, "", "Name of private key with which to sign")
c.Flags().String(FlagFrom, "", "Name or address of private key with which to sign")
c.Flags().Int64(FlagAccountNumber, 0, "AccountNumber number to sign the tx")
c.Flags().Int64(FlagSequence, 0, "Sequence number to sign the tx")
c.Flags().String(FlagMemo, "", "Memo to send along with transaction")
Expand Down
56 changes: 35 additions & 21 deletions client/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,44 @@ func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg)
return err
}

txCtxPtr, err := lookupTxCtx(txCtx, cliCtx, from)
if err != nil {
return err
}
txCtx = *txCtxPtr

name, err := cliCtx.GetFromName()
if err != nil {
return err
}
passphrase, err := keys.GetPassphrase(name)
if err != nil {
return err
}

if cliCtx.Gas == 0 {
txCtx, err = EnrichCtxWithGas(txCtx, cliCtx, name, passphrase, msgs)
if err != nil {
return err
}
}

// build and sign the transaction
txBytes, err := txCtx.BuildAndSign(name, passphrase, msgs)
if err != nil {
return err
}
// broadcast to a Tendermint node
return cliCtx.EnsureBroadcastTx(txBytes)
}

func lookupTxCtx(txCtx authctx.TxContext, cliCtx context.CLIContext, from []byte) (*authctx.TxContext, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did this come from? I don't think we should be returning pointers to contexts. Is this from the original simulation PR @alessio ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nvm, I don't see this on develop. I don't think lookupTxCtx is appropriate here. See prepareTxContext here. I think it should be analogous to that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was trying to reduce the cyclomatic complexity of the function above - I can revert.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohhh no, we should definitely reduce cyclomatic complexity! We just should:

  1. Not return a pointer
  2. Avoid a merge conflict cause @alessio's PR has something similar

That's all :-)

// TODO: (ref #1903) Allow for user supplied account number without
// automatically doing a manual lookup.
if txCtx.AccountNumber == 0 {
accNum, err := cliCtx.GetAccountNumber(from)
if err != nil {
return err
return nil, err
}

txCtx = txCtx.WithAccountNumber(accNum)
Expand All @@ -48,31 +80,13 @@ func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg)
if txCtx.Sequence == 0 {
accSeq, err := cliCtx.GetAccountSequence(from)
if err != nil {
return err
return nil, err
}

txCtx = txCtx.WithSequence(accSeq)
}

passphrase, err := keys.GetPassphrase(cliCtx.FromAddressName)
if err != nil {
return err
}

if cliCtx.Gas == 0 {
txCtx, err = EnrichCtxWithGas(txCtx, cliCtx, cliCtx.FromAddressName, passphrase, msgs)
if err != nil {
return err
}
}

// build and sign the transaction
txBytes, err := txCtx.BuildAndSign(cliCtx.FromAddressName, passphrase, msgs)
if err != nil {
return err
}
// broadcast to a Tendermint node
return cliCtx.EnsureBroadcastTx(txBytes)
return &txCtx, nil
}

// EnrichCtxWithGas calculates the gas estimate that would be consumed by the
Expand Down
29 changes: 28 additions & 1 deletion crypto/keys/keybase.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/tendermint/tendermint/crypto/encoding/amino"
"github.com/tendermint/tendermint/crypto/secp256k1"
dbm "github.com/tendermint/tendermint/libs/db"
"github.com/cosmos/cosmos-sdk/types"
)

var _ Keybase = dbKeybase{}
Expand Down Expand Up @@ -179,6 +180,13 @@ func (kb dbKeybase) List() ([]Info, error) {
iter := kb.db.Iterator(nil, nil)
defer iter.Close()
for ; iter.Valid(); iter.Next() {
key := string(iter.Key())

// need to exclude any keys in storage that have an address suffix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a more defensive mechanism would be to: if strings.HasSuffix(key, infoSuffix) { // read and parse...}

if strings.HasSuffix(key, ".address") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs a comment IMHO

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say if one needs to write code like ^^^, then something is wrong. Just a earlier warning to core devs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++

continue
}

info, err := readInfo(iter.Value())
if err != nil {
return nil, err
Expand All @@ -197,6 +205,15 @@ func (kb dbKeybase) Get(name string) (Info, error) {
return readInfo(bs)
}

func (kb dbKeybase) GetByAddress(address types.AccAddress) (Info, error) {
ik := kb.db.Get(addrKey((tmcrypto.Address)(address)))
if len(ik) == 0 {
return nil, fmt.Errorf("Key with address %s not found", address.String())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets lowercase Key 👍

}
bs := kb.db.Get(ik)
return readInfo(bs)
}

// Sign signs the msg with the named key.
// It returns an error if the key doesn't exist or the decryption fails.
func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig []byte, pub tmcrypto.PubKey, err error) {
Expand Down Expand Up @@ -341,16 +358,19 @@ func (kb dbKeybase) Delete(name, passphrase string) error {
if err != nil {
return err
}
kb.db.DeleteSync(addrKey(linfo.GetPubKey().Address()))
kb.db.DeleteSync(infoKey(name))
return nil
case ledgerInfo:
case offlineInfo:
if passphrase != "yes" {
return fmt.Errorf("enter 'yes' exactly to delete the key - this cannot be undone")
}
kb.db.DeleteSync(addrKey(info.GetPubKey().Address()))
kb.db.DeleteSync(infoKey(name))
return nil
}

return nil
}

Expand Down Expand Up @@ -407,7 +427,14 @@ func (kb dbKeybase) writeOfflineKey(pub tmcrypto.PubKey, name string) Info {

func (kb dbKeybase) writeInfo(info Info, name string) {
// write the info by key
kb.db.SetSync(infoKey(name), writeInfo(info))
key := infoKey(name)
kb.db.SetSync(key, writeInfo(info))
// store a pointer to the infokey by address for fast lookup
kb.db.SetSync(addrKey(info.GetPubKey().Address()), key)
}

func addrKey(address tmcrypto.Address) []byte {
return []byte(fmt.Sprintf("%s.address", address.String()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have more than one key type now, I think we should have these two suffixes as constants.

}

func infoKey(name string) []byte {
Expand Down
19 changes: 18 additions & 1 deletion crypto/keys/keybase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/tendermint/tendermint/crypto/ed25519"

dbm "github.com/tendermint/tendermint/libs/db"
"github.com/cosmos/cosmos-sdk/types"
)

func init() {
Expand All @@ -20,8 +21,9 @@ func init() {
// TestKeyManagement makes sure we can manipulate these keys well
func TestKeyManagement(t *testing.T) {
// make the storage with reasonable defaults
db := dbm.NewMemDB()
cstore := New(
dbm.NewMemDB(),
db,
)

algo := Secp256k1
Expand Down Expand Up @@ -51,6 +53,12 @@ func TestKeyManagement(t *testing.T) {
require.NoError(t, err)
_, err = cstore.Get(n3)
require.NotNil(t, err)
_, err = cstore.GetByAddress(accAddr(i2))
require.NoError(t, err)
addr, err := types.AccAddressFromBech32("cosmosaccaddr1jawd35d9aq4u76sr3fjalmcqc8hqygs9gtnmv3")
require.NoError(t, err)
_, err = cstore.GetByAddress(addr)
require.NotNil(t, err)

// list shows them in order
keyS, err := cstore.List()
Expand Down Expand Up @@ -92,6 +100,11 @@ func TestKeyManagement(t *testing.T) {
keyS, err = cstore.List()
require.NoError(t, err)
require.Equal(t, 1, len(keyS))

// addr cache gets nuked
err = cstore.Delete(n2, p2)
require.NoError(t, err)
require.False(t, db.Has(addrKey(i2.GetPubKey().Address())))
}

// TestSignVerify does some detailed checks on how we sign and validate
Expand Down Expand Up @@ -387,3 +400,7 @@ func ExampleNew() {
// Carl
// signed by Bob
}

func accAddr(info Info) types.AccAddress {
return (types.AccAddress)(info.GetPubKey().Address())
}
Loading