Skip to content

Commit

Permalink
Multi-signature workflow support (#3264)
Browse files Browse the repository at this point in the history
- New keys add --multisig flag to store multisig keys
  locally.
- New multisign command to generate multisig
  signatures.
- New sign --multisig flag to enable multisig mode.
- Add multisig transactions support in ante handler.
- gaiad add-genesis-account can now take both account
  addresses and key names.

Closes: #3198
  • Loading branch information
alessio authored and jackzampolin committed Jan 16, 2019
1 parent eff1f7c commit 26cb0a1
Show file tree
Hide file tree
Showing 18 changed files with 724 additions and 47 deletions.
7 changes: 6 additions & 1 deletion PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ FEATURES
* [\#2730](https://github.com/cosmos/cosmos-sdk/issues/2730) Add tx search pagination parameter
* [\#3027](https://github.com/cosmos/cosmos-sdk/issues/3027) Implement
`query gov proposer [proposal-id]` to query for a proposal's proposer.
* [\#3198](https://github.com/cosmos/cosmos-sdk/issues/3198) New `keys add --multisig` flag to store multisig keys locally.
* [\#3198](https://github.com/cosmos/cosmos-sdk/issues/3198) New `multisign` command to generate multisig signatures.
* [\#3198](https://github.com/cosmos/cosmos-sdk/issues/3198) New `sign --multisig` flag to enable multisig mode.

* Gaia
* [\#2182] [x/staking] Added querier for querying a single redelegation
* [\#2182] [x/staking] Added querier for querying a single redelegation
* [\#3198](https://github.com/cosmos/cosmos-sdk/issues/3198) [x/auth] Add multisig transactions support
* [\#3198](https://github.com/cosmos/cosmos-sdk/issues/3198) `add-genesis-account` can take both account addresses and key names

* SDK
* \#2694 Vesting account implementation.
Expand Down
3 changes: 2 additions & 1 deletion baseapp/baseapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
"bytes"
"encoding/binary"
"fmt"
"github.com/cosmos/cosmos-sdk/store"
"os"
"testing"

"github.com/cosmos/cosmos-sdk/store"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand Down
62 changes: 55 additions & 7 deletions client/keys/add.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package keys

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"sort"

"github.com/tendermint/tendermint/crypto/multisig"

"github.com/cosmos/go-bip39"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/libs/cli"

"github.com/cosmos/cosmos-sdk/client"
Expand All @@ -22,14 +27,15 @@ import (
)

const (
flagPublicKey = "pubkey"
flagInteractive = "interactive"
flagBIP44Path = "bip44-path"
flagRecover = "recover"
flagNoBackup = "no-backup"
flagDryRun = "dry-run"
flagAccount = "account"
flagIndex = "index"
flagMultisig = "multisig"
flagNoSort = "nosort"
)

func addKeyCommand() *cobra.Command {
Expand All @@ -43,13 +49,23 @@ and encrypted with the given password. The only input that is required is the en
If run with -i, it will prompt the user for BIP44 path, BIP39 mnemonic, and passphrase.
The flag --recover allows one to recover a key from a seed passphrase.
If run with --dry-run, a key would be generated (or recovered) but not stored to the local keystore.
Use the --pubkey flag to add arbitrary public keys to the keystore for constructing multisig transactions.
If run with --dry-run, a key would be generated (or recovered) but not stored to the
local keystore.
Use the --pubkey flag to add arbitrary public keys to the keystore for constructing
multisig transactions.
You can add a multisig key by passing the list of key names you want the public
key to be composed of to the --multisig flag and the minimum number of signatures
required through --multisig-threshold. The keys are sorted by address, unless
the flag --nosort is set.
`,
Args: cobra.ExactArgs(1),
RunE: runAddCmd,
}
cmd.Flags().String(FlagPublicKey, "", "Store only a public key (useful for constructing multisigs e.g. cosmospub1...)")
cmd.Flags().StringSlice(flagMultisig, nil, "Construct and store a multisig public key (implies --pubkey)")
cmd.Flags().Uint(flagMultiSigThreshold, 1, "K out of N required signatures. For use in conjunction with --multisig")
cmd.Flags().Bool(flagNoSort, false, "Keys passed to --multisig are taken in the order they're supplied")
cmd.Flags().String(FlagPublicKey, "", "Parse a public key in bech32 format and save it to disk")
cmd.Flags().BoolP(flagInteractive, "i", false, "Interactively prompt user for BIP39 passphrase and mnemonic")
cmd.Flags().Bool(client.FlagUseLedger, false, "Store a local reference to a private key on a Ledger device")
cmd.Flags().String(flagBIP44Path, "44'/118'/0'/0/0", "BIP44 path from which to derive a private key")
Expand Down Expand Up @@ -100,8 +116,40 @@ func runAddCmd(cmd *cobra.Command, args []string) error {
}
}

multisigKeys := viper.GetStringSlice(flagMultisig)
if len(multisigKeys) != 0 {
var pks []crypto.PubKey

multisigThreshold := viper.GetInt(flagMultiSigThreshold)
if err := validateMultisigThreshold(multisigThreshold, len(multisigKeys)); err != nil {
return err
}

for _, keyname := range multisigKeys {
k, err := kb.Get(keyname)
if err != nil {
return err
}
pks = append(pks, k.GetPubKey())
}

// Handle --nosort
if !viper.GetBool(flagNoSort) {
sort.Slice(pks, func(i, j int) bool {
return bytes.Compare(pks[i].Address(), pks[j].Address()) < 0
})
}

pk := multisig.NewPubKeyMultisigThreshold(multisigThreshold, pks)
if _, err := kb.CreateOffline(name, pk); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Key %q saved to disk.", name)
return nil
}

// ask for a password when generating a local key
if viper.GetString(flagPublicKey) == "" && !viper.GetBool(client.FlagUseLedger) {
if viper.GetString(FlagPublicKey) == "" && !viper.GetBool(client.FlagUseLedger) {
encryptPassword, err = client.GetCheckPassword(
"Enter a passphrase to encrypt your key to disk:",
"Repeat the passphrase:", buf)
Expand All @@ -111,8 +159,8 @@ func runAddCmd(cmd *cobra.Command, args []string) error {
}
}

if viper.GetString(flagPublicKey) != "" {
pk, err := sdk.GetAccPubKeyBech32(viper.GetString(flagPublicKey))
if viper.GetString(FlagPublicKey) != "" {
pk, err := sdk.GetAccPubKeyBech32(viper.GetString(FlagPublicKey))
if err != nil {
return err
}
Expand Down
55 changes: 48 additions & 7 deletions client/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,28 +133,69 @@ func SignStdTx(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, name string,
"The generated transaction's intended signer does not match the given signer: %q", name)
}

if !offline && txBldr.GetAccountNumber() == 0 {
accNum, err := cliCtx.GetAccountNumber(addr)
if !offline {
txBldr, err = populateAccountFromState(
txBldr, cliCtx, sdk.AccAddress(addr))
if err != nil {
return signedStdTx, err
}
txBldr = txBldr.WithAccountNumber(accNum)
}

if !offline && txBldr.GetSequence() == 0 {
accSeq, err := cliCtx.GetAccountSequence(addr)
passphrase, err := keys.GetPassphrase(name)
if err != nil {
return signedStdTx, err
}

return txBldr.SignStdTx(name, passphrase, stdTx, appendSig)
}

// SignStdTxWithSignerAddress attaches a signature to a StdTx and returns a copy of a it.
// Don't perform online validation or lookups if offline is true, else
// populate account and sequence numbers from a foreign account.
func SignStdTxWithSignerAddress(txBldr authtxb.TxBuilder, cliCtx context.CLIContext,
addr sdk.AccAddress, name string, stdTx auth.StdTx,
offline bool) (signedStdTx auth.StdTx, err error) {

// check whether the address is a signer
if !isTxSigner(addr, stdTx.GetSigners()) {
return signedStdTx, fmt.Errorf(
"The generated transaction's intended signer does not match the given signer: %q", name)
}

if !offline {
txBldr, err = populateAccountFromState(txBldr, cliCtx, addr)
if err != nil {
return signedStdTx, err
}
txBldr = txBldr.WithSequence(accSeq)
}

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

return txBldr.SignStdTx(name, passphrase, stdTx, appendSig)
return txBldr.SignStdTx(name, passphrase, stdTx, false)
}

func populateAccountFromState(txBldr authtxb.TxBuilder, cliCtx context.CLIContext,
addr sdk.AccAddress) (authtxb.TxBuilder, error) {
if txBldr.GetAccountNumber() == 0 {
accNum, err := cliCtx.GetAccountNumber(addr)
if err != nil {
return txBldr, err
}
txBldr = txBldr.WithAccountNumber(accNum)
}

if txBldr.GetSequence() == 0 {
accSeq, err := cliCtx.GetAccountSequence(addr)
if err != nil {
return txBldr, err
}
txBldr = txBldr.WithSequence(accSeq)
}

return txBldr, nil
}

// GetTxEncoder return tx encoder from global sdk configuration if ones is defined.
Expand Down
Loading

0 comments on commit 26cb0a1

Please sign in to comment.