Multi-signature workflow support (#3264)
- 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
This commit is contained in:
committed by
Jack Zampolin
parent
eff1f7ca10
commit
26cb0a125a
+55
-7
@@ -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"
|
||||
@@ -22,7 +27,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
flagPublicKey = "pubkey"
|
||||
flagInteractive = "interactive"
|
||||
flagBIP44Path = "bip44-path"
|
||||
flagRecover = "recover"
|
||||
@@ -30,6 +34,8 @@ const (
|
||||
flagDryRun = "dry-run"
|
||||
flagAccount = "account"
|
||||
flagIndex = "index"
|
||||
flagMultisig = "multisig"
|
||||
flagNoSort = "nosort"
|
||||
)
|
||||
|
||||
func addKeyCommand() *cobra.Command {
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+52
-11
@@ -133,20 +133,12 @@ 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)
|
||||
if err != nil {
|
||||
return signedStdTx, err
|
||||
}
|
||||
txBldr = txBldr.WithSequence(accSeq)
|
||||
}
|
||||
|
||||
passphrase, err := keys.GetPassphrase(name)
|
||||
@@ -157,6 +149,55 @@ func SignStdTx(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, name string,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
passphrase, err := keys.GetPassphrase(name)
|
||||
if err != nil {
|
||||
return signedStdTx, err
|
||||
}
|
||||
|
||||
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.
|
||||
// Otherwise returns encoder with default logic.
|
||||
func GetTxEncoder(cdc *codec.Codec) (encoder sdk.TxEncoder) {
|
||||
|
||||
Reference in New Issue
Block a user