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:
Alessio Treglia 2019-01-16 17:30:57 +00:00 committed by Jack Zampolin
parent eff1f7ca10
commit 26cb0a125a
18 changed files with 728 additions and 51 deletions

View File

@ -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.

View File

@ -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"

View File

@ -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
}

View File

@ -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) {

View File

@ -25,6 +25,26 @@ import (
stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)
func TestGaiaCLIKeysAddMultisig(t *testing.T) {
t.Parallel()
f := InitFixtures(t)
// key names order does not matter
f.KeysAdd("msig1", "--multisig-threshold=2",
fmt.Sprintf("--multisig=%s,%s", keyBar, keyBaz))
f.KeysAdd("msig2", "--multisig-threshold=2",
fmt.Sprintf("--multisig=%s,%s", keyBaz, keyBar))
require.Equal(t, f.KeysShow("msig1").Address, f.KeysShow("msig2").Address)
f.KeysAdd("msig3", "--multisig-threshold=2",
fmt.Sprintf("--multisig=%s,%s", keyBar, keyBaz),
"--nosort")
f.KeysAdd("msig4", "--multisig-threshold=2",
fmt.Sprintf("--multisig=%s,%s", keyBaz, keyBar),
"--nosort")
require.NotEqual(t, f.KeysShow("msig3").Address, f.KeysShow("msig4").Address)
}
func TestGaiaCLIMinimumFees(t *testing.T) {
t.Parallel()
f := InitFixtures(t)
@ -647,6 +667,180 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) {
f.Cleanup()
}
func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) {
t.Parallel()
f := InitFixtures(t)
// start gaiad server with minimum fees
proc := f.GDStart()
defer proc.Stop(false)
fooBarBazAddr := f.KeyAddress(keyFooBarBaz)
barAddr := f.KeyAddress(keyBar)
// Send some tokens from one account to the other
success, _, _ := f.TxSend(keyFoo, fooBarBazAddr, sdk.NewInt64Coin(denom, 10))
require.True(t, success)
tests.WaitForNextNBlocksTM(1, f.Port)
// Test generate sendTx with multisig
success, stdout, _ := f.TxSend(keyFooBarBaz, barAddr, sdk.NewInt64Coin(denom, 5), "--generate-only")
require.True(t, success)
// Write the output to disk
unsignedTxFile := writeToNewTempFile(t, stdout)
defer os.Remove(unsignedTxFile.Name())
// Sign with foo's key
success, stdout, _ = f.TxSign(keyFoo, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String())
require.True(t, success)
// Write the output to disk
fooSignatureFile := writeToNewTempFile(t, stdout)
defer os.Remove(fooSignatureFile.Name())
// Multisign, not enough signatures
success, stdout, _ = f.TxMultisign(unsignedTxFile.Name(), keyFooBarBaz, []string{fooSignatureFile.Name()})
require.True(t, success)
// Write the output to disk
signedTxFile := writeToNewTempFile(t, stdout)
defer os.Remove(signedTxFile.Name())
// Validate the multisignature
success, _, _ = f.TxSign(keyFooBarBaz, signedTxFile.Name(), "--validate-signatures", "--json")
require.False(t, success)
// Broadcast the transaction
success, _, _ = f.TxBroadcast(signedTxFile.Name())
require.False(t, success)
}
func TestGaiaCLIMultisignSortSignatures(t *testing.T) {
t.Parallel()
f := InitFixtures(t)
// start gaiad server with minimum fees
proc := f.GDStart()
defer proc.Stop(false)
fooBarBazAddr := f.KeyAddress(keyFooBarBaz)
barAddr := f.KeyAddress(keyBar)
// Send some tokens from one account to the other
success, _, _ := f.TxSend(keyFoo, fooBarBazAddr, sdk.NewInt64Coin(denom, 10))
require.True(t, success)
tests.WaitForNextNBlocksTM(1, f.Port)
// Ensure account balances match expected
fooBarBazAcc := f.QueryAccount(fooBarBazAddr)
require.Equal(t, int64(10), fooBarBazAcc.GetCoins().AmountOf(denom).Int64())
// Test generate sendTx with multisig
success, stdout, _ := f.TxSend(keyFooBarBaz, barAddr, sdk.NewInt64Coin(denom, 5), "--generate-only")
require.True(t, success)
// Write the output to disk
unsignedTxFile := writeToNewTempFile(t, stdout)
defer os.Remove(unsignedTxFile.Name())
// Sign with foo's key
success, stdout, _ = f.TxSign(keyFoo, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String())
require.True(t, success)
// Write the output to disk
fooSignatureFile := writeToNewTempFile(t, stdout)
defer os.Remove(fooSignatureFile.Name())
// Sign with baz's key
success, stdout, _ = f.TxSign(keyBaz, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String())
require.True(t, success)
// Write the output to disk
bazSignatureFile := writeToNewTempFile(t, stdout)
defer os.Remove(bazSignatureFile.Name())
// Multisign, keys in different order
success, stdout, _ = f.TxMultisign(unsignedTxFile.Name(), keyFooBarBaz, []string{
bazSignatureFile.Name(), fooSignatureFile.Name()})
require.True(t, success)
// Write the output to disk
signedTxFile := writeToNewTempFile(t, stdout)
defer os.Remove(signedTxFile.Name())
// Validate the multisignature
success, _, _ = f.TxSign(keyFooBarBaz, signedTxFile.Name(), "--validate-signatures", "--json")
require.True(t, success)
// Broadcast the transaction
success, _, _ = f.TxBroadcast(signedTxFile.Name())
require.True(t, success)
}
func TestGaiaCLIMultisign(t *testing.T) {
t.Parallel()
f := InitFixtures(t)
// start gaiad server with minimum fees
proc := f.GDStart()
defer proc.Stop(false)
fooBarBazAddr := f.KeyAddress(keyFooBarBaz)
bazAddr := f.KeyAddress(keyBaz)
// Send some tokens from one account to the other
success, _, _ := f.TxSend(keyFoo, fooBarBazAddr, sdk.NewInt64Coin(denom, 10))
require.True(t, success)
tests.WaitForNextNBlocksTM(1, f.Port)
// Ensure account balances match expected
fooBarBazAcc := f.QueryAccount(fooBarBazAddr)
require.Equal(t, int64(10), fooBarBazAcc.GetCoins().AmountOf(denom).Int64())
// Test generate sendTx with multisig
success, stdout, stderr := f.TxSend(keyFooBarBaz, bazAddr, sdk.NewInt64Coin(denom, 10), "--generate-only")
require.True(t, success)
require.Empty(t, stderr)
// Write the output to disk
unsignedTxFile := writeToNewTempFile(t, stdout)
defer os.Remove(unsignedTxFile.Name())
// Sign with foo's key
success, stdout, _ = f.TxSign(keyFoo, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String())
require.True(t, success)
// Write the output to disk
fooSignatureFile := writeToNewTempFile(t, stdout)
defer os.Remove(fooSignatureFile.Name())
// Sign with bar's key
success, stdout, _ = f.TxSign(keyBar, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String())
require.True(t, success)
// Write the output to disk
barSignatureFile := writeToNewTempFile(t, stdout)
defer os.Remove(barSignatureFile.Name())
// Multisign
success, stdout, _ = f.TxMultisign(unsignedTxFile.Name(), keyFooBarBaz, []string{
fooSignatureFile.Name(), barSignatureFile.Name()})
require.True(t, success)
// Write the output to disk
signedTxFile := writeToNewTempFile(t, stdout)
defer os.Remove(signedTxFile.Name())
// Validate the multisignature
success, _, _ = f.TxSign(keyFooBarBaz, signedTxFile.Name(), "--validate-signatures", "--json")
require.True(t, success)
// Broadcast the transaction
success, _, _ = f.TxBroadcast(signedTxFile.Name())
require.True(t, success)
}
func TestGaiaCLIConfig(t *testing.T) {
t.Parallel()
f := InitFixtures(t)

View File

@ -27,11 +27,13 @@ import (
)
const (
denom = "stake"
keyFoo = "foo"
keyBar = "bar"
fooDenom = "footoken"
feeDenom = "feetoken"
denom = "stake"
keyFoo = "foo"
keyBar = "bar"
keyBaz = "baz"
keyFooBarBaz = "foobarbaz"
fooDenom = "footoken"
feeDenom = "feetoken"
)
var startCoins = sdk.Coins{
@ -83,8 +85,13 @@ func InitFixtures(t *testing.T) (f *Fixtures) {
// Ensure keystore has foo and bar keys
f.KeysDelete(keyFoo)
f.KeysDelete(keyBar)
f.KeysDelete(keyBar)
f.KeysDelete(keyFooBarBaz)
f.KeysAdd(keyFoo)
f.KeysAdd(keyBar)
f.KeysAdd(keyBaz)
f.KeysAdd(keyFooBarBaz, "--multisig-threshold=2", fmt.Sprintf(
"--multisig=%s,%s,%s", keyFoo, keyBar, keyBaz))
// Ensure that CLI output is in JSON format
f.CLIConfig("output", "json")
@ -175,7 +182,7 @@ func (f *Fixtures) GDStart(flags ...string) *tests.Process {
// KeysDelete is gaiacli keys delete
func (f *Fixtures) KeysDelete(name string, flags ...string) {
cmd := fmt.Sprintf("gaiacli keys delete --home=%s %s", f.GCLIHome, name)
executeWrite(f.T, addFlags(cmd, flags), app.DefaultKeyPass)
executeWrite(f.T, addFlags(cmd, append(append(flags, "-y"), "-f")))
}
// KeysAdd is gaiacli keys add
@ -232,6 +239,16 @@ func (f *Fixtures) TxBroadcast(fileName string, flags ...string) (bool, string,
return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass)
}
// TxMultisign is gaiacli tx multisign
func (f *Fixtures) TxMultisign(fileName, name string, signaturesFiles []string,
flags ...string) (bool, string, string) {
cmd := fmt.Sprintf("gaiacli tx multisign %v %s %s %s", f.Flags(),
fileName, name, strings.Join(signaturesFiles, " "),
)
return executeWriteRetStdStreams(f.T, cmd)
}
//___________________________________________________________________________________
// gaiacli tx staking

View File

@ -136,6 +136,7 @@ func txCmd(cdc *amino.Codec, mc []sdk.ModuleClients) *cobra.Command {
bankcmd.SendTxCmd(cdc),
client.LineBreak,
authcmd.GetSignCommand(cdc),
authcmd.GetMultiSignCommand(cdc),
bankcmd.GetBroadcastCommand(cdc),
client.LineBreak,
)

View File

@ -2,10 +2,10 @@ package main
import (
"encoding/json"
"github.com/cosmos/cosmos-sdk/store"
"io"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/store"
"github.com/spf13/cobra"
"github.com/spf13/viper"

View File

@ -2,12 +2,13 @@ package main
import (
"fmt"
"github.com/cosmos/cosmos-sdk/store"
"io"
"os"
"path/filepath"
"time"
"github.com/cosmos/cosmos-sdk/store"
cpm "github.com/otiai10/copy"
"github.com/spf13/cobra"

View File

@ -9,6 +9,7 @@ import (
"github.com/tendermint/tendermint/libs/cli"
"github.com/tendermint/tendermint/libs/common"
"github.com/cosmos/cosmos-sdk/client/keys"
"github.com/cosmos/cosmos-sdk/cmd/gaia/app"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/server"
@ -19,7 +20,7 @@ import (
// AddGenesisAccountCmd returns add-genesis-account cobra Command
func AddGenesisAccountCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "add-genesis-account [address] [coin][,[coin]]",
Use: "add-genesis-account [address_or_key_name] [coin][,[coin]]",
Short: "Add genesis account to genesis.json",
Args: cobra.ExactArgs(2),
RunE: func(_ *cobra.Command, args []string) error {
@ -28,7 +29,15 @@ func AddGenesisAccountCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command
addr, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
kb, err := keys.GetKeyBaseFromDir(viper.GetString(flagClientHome))
if err != nil {
return err
}
info, err := kb.Get(args[0])
if err != nil {
return err
}
addr = info.GetAddress()
}
coins, err := sdk.ParseCoins(args[1])
if err != nil {
@ -60,6 +69,7 @@ func AddGenesisAccountCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command
}
cmd.Flags().String(cli.HomeFlag, app.DefaultNodeHome, "node's home directory")
cmd.Flags().String(flagClientHome, app.DefaultCLIHome, "client's home directory")
return cmd
}

View File

@ -3,6 +3,7 @@ package cli
import (
"encoding/hex"
"fmt"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/utils"
"github.com/cosmos/cosmos-sdk/codec"

View File

@ -87,15 +87,36 @@ Note that this is the Tendermint signing key, _not_ the operator key you will us
We strongly recommend _NOT_ using the same passphrase for multiple keys. The Tendermint team and the Interchain Foundation will not be responsible for the loss of funds.
:::
#### Multisig public keys
#### Generate multisig public keys
You can generate and print a multisig public key by typing:
```bash
gaiacli show --multisig-threshold K name1 name2 name3 [...]
gaiacli keys add --multisig=name1,name2,name3[...] --multisig-threshold=K new_key_name
```
`K` is the minimum weight, e.g. minimum number of private keys that must have signed the transactions that carry the generated public key.
`K` is the minimum number of private keys that must have signed the
transactions that carry the public key's address as signer.
The `--multisig` flag must contain the name of public keys that will be combined into a
public key that will be generated and stored as `new_key_name` in the local database.
All names supplied through `--multisig` must already exist in the local database. Unless
the flag `--nosort` is set, the order in which the keys are supplied on the command line
does not matter, i.e. the following commands generate two identical keys:
```bash
gaiacli keys add --multisig=foo,bar,baz --multisig-threshold=2 multisig_address
gaiacli keys add --multisig=baz,foo,bar --multisig-threshold=2 multisig_address
```
Multisig addresses can also be generated on-the-fly and printed through the which command:
```bash
gaiacli keys show --multisig-threshold K name1 name2 name3 [...]
```
For more information regarding how to generate, sign and broadcast transactions with a
multi signature account see [Multisig Transactions](#multisig-transactions).
### Account
@ -182,7 +203,7 @@ gaiacli tx sign \
unsignedSendTx.json > signedSendTx.json
```
You can validate the transaction's signagures by typing the following:
You can validate the transaction's signatures by typing the following:
```bash
gaiacli tx sign --validate-signatures signedSendTx.json
@ -576,3 +597,88 @@ gaiacli query gov param voting
gaiacli query gov param tallying
gaiacli query gov param deposit
```
### Multisig transactions
Multisig transactions require signatures of multiple private keys. Thus, generating and signing
a transaction from a multisig account involve cooperation among the parties involved. A multisig
transaction can be initiated by any of the key holders, and at least one of them would need to
import other parties' public keys into their local database and generate a multisig public key
in order to finalize and broadcast the transaction.
For example, given a multisig key comprising the keys `p1`, `p2`, and `p3`, each of which is held
by a distinct party, the user holding `p1` would require to import both `p2` and `p3` in order to
generate the multisig account public key:
```
gaiacli keys add \
--pubkey=cosmospub1addwnpepqtd28uwa0yxtwal5223qqr5aqf5y57tc7kk7z8qd4zplrdlk5ez5kdnlrj4 \
p2
gaiacli keys add \
--pubkey=cosmospub1addwnpepqgj04jpm9wrdml5qnss9kjxkmxzywuklnkj0g3a3f8l5wx9z4ennz84ym5t \
p3
gaiacli keys add \
--multisig-threshold=2
--multisig=p1,p2,p3
p1p2p3
```
A new multisig public key `p1p2p3` has been stored, and its address will be
used as signer of multisig transactions:
```bash
gaiacli keys show --address p1p2p3
```
The first step to create a multisig transaction is to initiate it on behalf
of the multisig address created above:
```bash
gaiacli tx send \
--from=<multisig_address> \
--to=cosmos1570v2fq3twt0f0x02vhxpuzc9jc4yl30q2qned \
--amount=10stake \
--generate-only > unsignedTx.json
```
The file `unsignedTx.json` contains the unsigned transaction encoded in JSON.
`p1` can now sign the transaction with its own private key:
```bash
gaiacli tx sign \
--multisig=<multisig_address> \
--name=p1 \
--output-document=p1signature.json \
unsignedTx.json
```
Once the signature is generated, `p1` transmits both `unsignedTx.json` and
`p1signature.json` to `p2` or `p3`, which in turn will generate their
respective signature:
```bash
gaiacli tx sign \
--multisig=<multisig_address> \
--name=p2 \
--output-document=p2signature.json \
unsignedTx.json
```
`p1p2p3` is a 2-of-3 multisig key, therefore one additional signature
is sufficient. Any the key holders can now generate the multisig
transaction by combining the required signature files:
```bash
gaiacli tx multisign \
unsignedTx.json \
p1p2p3 \
p1signature.json p2signature.json > signedTx.json
```
The transaction can now be sent to the node:
```bash
gaiacli tx broadcast signedTx.json
```

View File

@ -8,8 +8,10 @@ import (
"time"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/multisig"
"github.com/tendermint/tendermint/crypto/secp256k1"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
)
@ -168,7 +170,7 @@ func processSig(
return nil, sdk.ErrInternal("setting PubKey on signer's account").Result()
}
consumeSignatureVerificationGas(ctx.GasMeter(), pubKey, params)
consumeSignatureVerificationGas(ctx.GasMeter(), sig.Signature, pubKey, params)
if !simulate && !pubKey.VerifyBytes(signBytes, sig.Signature) {
return nil, sdk.ErrUnauthorized("signature verification failed").Result()
}
@ -227,18 +229,39 @@ func ProcessPubKey(acc Account, sig StdSignature, simulate bool) (crypto.PubKey,
// matched by the concrete type.
//
// TODO: Design a cleaner and flexible way to match concrete public key types.
func consumeSignatureVerificationGas(meter sdk.GasMeter, pubkey crypto.PubKey, params Params) {
func consumeSignatureVerificationGas(meter sdk.GasMeter, sig []byte, pubkey crypto.PubKey, params Params) {
pubkeyType := strings.ToLower(fmt.Sprintf("%T", pubkey))
switch {
case strings.Contains(pubkeyType, "ed25519"):
meter.ConsumeGas(params.SigVerifyCostED25519, "ante verify: ed25519")
case strings.Contains(pubkeyType, "secp256k1"):
meter.ConsumeGas(params.SigVerifyCostSecp256k1, "ante verify: secp256k1")
case strings.Contains(pubkeyType, "multisigthreshold"):
var multisignature multisig.Multisignature
codec.Cdc.MustUnmarshalBinaryBare(sig, &multisignature)
multisigPubKey := pubkey.(multisig.PubKeyMultisigThreshold)
consumeMultisignatureVerificationGas(meter, multisignature, multisigPubKey, params)
default:
panic(fmt.Sprintf("unrecognized signature type: %s", pubkeyType))
}
}
func consumeMultisignatureVerificationGas(meter sdk.GasMeter,
sig multisig.Multisignature, pubkey multisig.PubKeyMultisigThreshold,
params Params) {
size := sig.BitArray.Size()
sigIndex := 0
for i := 0; i < size; i++ {
if sig.BitArray.GetIndex(i) {
consumeSignatureVerificationGas(meter, sig.Sigs[sigIndex], pubkey.PubKeys[i], params)
sigIndex++
}
}
}
func adjustFeesByGas(fees sdk.Coins, gas uint64) sdk.Coins {
gasCost := gas / gasPerUnitCost
gasFees := make(sdk.Coins, len(fees))

View File

@ -2,6 +2,7 @@ package auth
import (
"fmt"
"math/rand"
"strings"
"testing"
@ -564,9 +565,19 @@ func TestProcessPubKey(t *testing.T) {
func TestConsumeSignatureVerificationGas(t *testing.T) {
params := DefaultParams()
msg := []byte{1, 2, 3, 4}
pkSet1, sigSet1 := generatePubKeysAndSignatures(5, msg, false)
multisigKey1 := multisig.NewPubKeyMultisigThreshold(2, pkSet1)
multisignature1 := multisig.NewMultisig(len(pkSet1))
expectedCost1 := expectedGasCostByKeys(pkSet1)
for i := 0; i < len(pkSet1); i++ {
multisignature1.AddSignatureFromPubKey(sigSet1[i], pkSet1[i], pkSet1)
}
type args struct {
meter sdk.GasMeter
sig []byte
pubkey crypto.PubKey
params Params
}
@ -576,22 +587,54 @@ func TestConsumeSignatureVerificationGas(t *testing.T) {
gasConsumed uint64
wantPanic bool
}{
{"PubKeyEd25519", args{sdk.NewInfiniteGasMeter(), ed25519.GenPrivKey().PubKey(), params}, DefaultSigVerifyCostED25519, false},
{"PubKeySecp256k1", args{sdk.NewInfiniteGasMeter(), secp256k1.GenPrivKey().PubKey(), params}, DefaultSigVerifyCostSecp256k1, false},
{"unknown key", args{sdk.NewInfiniteGasMeter(), nil, params}, 0, true},
{"PubKeyEd25519", args{sdk.NewInfiniteGasMeter(), nil, ed25519.GenPrivKey().PubKey(), params}, DefaultSigVerifyCostED25519, false},
{"PubKeySecp256k1", args{sdk.NewInfiniteGasMeter(), nil, secp256k1.GenPrivKey().PubKey(), params}, DefaultSigVerifyCostSecp256k1, false},
{"Multisig", args{sdk.NewInfiniteGasMeter(), multisignature1.Marshal(), multisigKey1, params}, expectedCost1, false},
{"unknown key", args{sdk.NewInfiniteGasMeter(), nil, nil, params}, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantPanic {
require.Panics(t, func() { consumeSignatureVerificationGas(tt.args.meter, tt.args.pubkey, tt.args.params) })
require.Panics(t, func() { consumeSignatureVerificationGas(tt.args.meter, tt.args.sig, tt.args.pubkey, tt.args.params) })
} else {
consumeSignatureVerificationGas(tt.args.meter, tt.args.pubkey, tt.args.params)
require.Equal(t, tt.args.meter.GasConsumed(), tt.gasConsumed)
consumeSignatureVerificationGas(tt.args.meter, tt.args.sig, tt.args.pubkey, tt.args.params)
require.Equal(t, tt.gasConsumed, tt.args.meter.GasConsumed(), fmt.Sprintf("%d != %d", tt.gasConsumed, tt.args.meter.GasConsumed()))
}
})
}
}
func generatePubKeysAndSignatures(n int, msg []byte, keyTypeed25519 bool) (pubkeys []crypto.PubKey, signatures [][]byte) {
pubkeys = make([]crypto.PubKey, n)
signatures = make([][]byte, n)
for i := 0; i < n; i++ {
var privkey crypto.PrivKey
if rand.Int63()%2 == 0 {
privkey = ed25519.GenPrivKey()
} else {
privkey = secp256k1.GenPrivKey()
}
pubkeys[i] = privkey.PubKey()
signatures[i], _ = privkey.Sign(msg)
}
return
}
func expectedGasCostByKeys(pubkeys []crypto.PubKey) uint64 {
cost := uint64(0)
for _, pubkey := range pubkeys {
pubkeyType := strings.ToLower(fmt.Sprintf("%T", pubkey))
switch {
case strings.Contains(pubkeyType, "ed25519"):
cost += DefaultParams().SigVerifyCostED25519
case strings.Contains(pubkeyType, "secp256k1"):
cost += DefaultParams().SigVerifyCostSecp256k1
default:
panic("unexpected key type")
}
}
return cost
}
func TestAdjustFeesByGas(t *testing.T) {
type args struct {
fee sdk.Coins

View File

@ -0,0 +1,159 @@
package cli
import (
"fmt"
"io/ioutil"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
amino "github.com/tendermint/go-amino"
"github.com/tendermint/tendermint/crypto/multisig"
"github.com/tendermint/tendermint/libs/cli"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/keys"
crkeys "github.com/cosmos/cosmos-sdk/crypto/keys"
"github.com/cosmos/cosmos-sdk/x/auth"
authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder"
)
// GetSignCommand returns the sign command
func GetMultiSignCommand(codec *amino.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "multisign <file> <name> <<signature>...>",
Short: "Generate multisig signatures for transactions generated offline",
Long: `Sign transactions created with the --generate-only flag that require multisig signatures.
Read signature(s) from <signature> file(s), generate a multisig signature compliant to the
multisig key <name>, and attach it to the transaction read from <file>. Example:
gaiacli multisign transaction.json k1k2k3 k1sig.json k2sig.json k3sig.json
If the flag --signature-only flag is on, it outputs a JSON representation
of the generated signature only.
The --offline flag makes sure that the client will not reach out to an external node.
Thus account number or sequence number lookups will not be performed and it is
recommended to set such parameters manually.
`,
RunE: makeMultiSignCmd(codec),
Args: cobra.MinimumNArgs(3),
}
cmd.Flags().Bool(flagSigOnly, false, "Print only the generated signature, then exit")
cmd.Flags().Bool(flagOffline, false, "Offline mode. Do not query a full node")
cmd.Flags().String(flagOutfile, "",
"The document will be written to the given file instead of STDOUT")
// Add the flags here and return the command
return client.PostCommands(cmd)[0]
}
func makeMultiSignCmd(cdc *amino.Codec) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) (err error) {
stdTx, err := readAndUnmarshalStdTx(cdc, args[0])
if err != nil {
return
}
keybase, err := keys.GetKeyBaseFromDir(viper.GetString(cli.HomeFlag))
if err != nil {
return
}
multisigInfo, err := keybase.Get(args[1])
if err != nil {
return
}
if multisigInfo.GetType() != crkeys.TypeOffline {
return fmt.Errorf("%q must be of type offline: %s",
args[1], multisigInfo.GetType())
}
multisigPub := multisigInfo.GetPubKey().(multisig.PubKeyMultisigThreshold)
multisigSig := multisig.NewMultisig(len(multisigPub.PubKeys))
cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(cdc)
txBldr := authtxb.NewTxBuilderFromCLI()
if !viper.GetBool(flagOffline) {
addr := multisigInfo.GetAddress()
accnum, err := cliCtx.GetAccountNumber(addr)
if err != nil {
return err
}
seq, err := cliCtx.GetAccountSequence(addr)
if err != nil {
return err
}
txBldr = txBldr.WithAccountNumber(accnum).WithSequence(seq)
}
// read each signature and add it to the multisig if valid
for i := 2; i < len(args); i++ {
stdSig, err := readAndUnmarshalStdSignature(cdc, args[i])
if err != nil {
return err
}
// Validate each signature
sigBytes := auth.StdSignBytes(
txBldr.GetChainID(), txBldr.GetAccountNumber(), txBldr.GetSequence(),
stdTx.Fee, stdTx.GetMsgs(), stdTx.GetMemo(),
)
if ok := stdSig.PubKey.VerifyBytes(sigBytes, stdSig.Signature); !ok {
return fmt.Errorf("couldn't verify signature")
}
multisigSig.AddSignatureFromPubKey(stdSig.Signature, stdSig.PubKey, multisigPub.PubKeys)
}
newStdSig := auth.StdSignature{Signature: cdc.MustMarshalBinaryBare(multisigSig), PubKey: multisigPub}
newTx := auth.NewStdTx(stdTx.GetMsgs(), stdTx.Fee, []auth.StdSignature{newStdSig}, stdTx.GetMemo())
sigOnly := viper.GetBool(flagSigOnly)
var json []byte
switch {
case sigOnly && cliCtx.Indent:
json, err = cdc.MarshalJSONIndent(newTx.Signatures[0], "", " ")
case sigOnly && !cliCtx.Indent:
json, err = cdc.MarshalJSON(newTx.Signatures[0])
case !sigOnly && cliCtx.Indent:
json, err = cdc.MarshalJSONIndent(newTx, "", " ")
default:
json, err = cdc.MarshalJSON(newTx)
}
if err != nil {
return err
}
if viper.GetString(flagOutfile) == "" {
fmt.Printf("%s\n", json)
return
}
fp, err := os.OpenFile(
viper.GetString(flagOutfile), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644,
)
if err != nil {
return err
}
defer fp.Close()
fmt.Fprintf(fp, "%s\n", json)
return
}
}
func readAndUnmarshalStdSignature(cdc *amino.Codec, filename string) (stdSig auth.StdSignature, err error) {
var bytes []byte
if bytes, err = ioutil.ReadFile(filename); err != nil {
return
}
if err = cdc.UnmarshalJSON(bytes, &stdSig); err != nil {
return
}
return
}

View File

@ -19,6 +19,7 @@ import (
)
const (
flagMultisig = "multisig"
flagAppend = "append"
flagValidateSigs = "validate-signatures"
flagOffline = "offline"
@ -45,17 +46,26 @@ performed as that will require communication with a full node.
The --offline flag makes sure that the client will not reach out to an external node.
Thus account number or sequence number lookups will not be performed and it is
recommended to set such parameters manually.`,
recommended to set such parameters manually.
The --multisig=<multisig_key> flag generates a signature on behalf of a multisig account
key. It implies --signature-only. Full multisig signed transactions may eventually
be generated via the 'multisign' command.
`,
RunE: makeSignCmd(codec),
Args: cobra.ExactArgs(1),
}
cmd.Flags().String(client.FlagName, "", "Name of private key with which to sign")
cmd.Flags().String(flagMultisig, "",
"Address of the multisig account on behalf of which the "+
"transaction shall be signed")
cmd.Flags().Bool(flagAppend, true,
"Append the signature to the existing ones. If disabled, old signatures would be overwritten")
cmd.Flags().Bool(flagSigOnly, false, "Print only the generated signature, then exit.")
"Append the signature to the existing ones. "+
"If disabled, old signatures would be overwritten. Ignored if --multisig is on")
cmd.Flags().Bool(flagSigOnly, false, "Print only the generated signature, then exit")
cmd.Flags().Bool(flagValidateSigs, false, "Print the addresses that must sign the transaction, "+
"those who have already signed it, and make sure that signatures are in the correct order.")
cmd.Flags().Bool(flagOffline, false, "Offline mode. Do not query a full node.")
"those who have already signed it, and make sure that signatures are in the correct order")
cmd.Flags().Bool(flagOffline, false, "Offline mode. Do not query a full node")
cmd.Flags().String(flagOutfile, "",
"The document will be written to the given file instead of STDOUT")
@ -88,9 +98,25 @@ func makeSignCmd(cdc *amino.Codec) func(cmd *cobra.Command, args []string) error
}
// if --signature-only is on, then override --append
var newTx auth.StdTx
generateSignatureOnly := viper.GetBool(flagSigOnly)
appendSig := viper.GetBool(flagAppend) && !generateSignatureOnly
newTx, err := utils.SignStdTx(txBldr, cliCtx, name, stdTx, appendSig, offline)
multisigAddrStr := viper.GetString(flagMultisig)
if multisigAddrStr != "" {
var multisigAddr sdk.AccAddress
multisigAddr, err = sdk.AccAddressFromBech32(multisigAddrStr)
if err != nil {
return err
}
newTx, err = utils.SignStdTxWithSignerAddress(
txBldr, cliCtx, multisigAddr, name, stdTx, offline)
generateSignatureOnly = true
} else {
appendSig := viper.GetBool(flagAppend) && !generateSignatureOnly
newTx, err = utils.SignStdTx(
txBldr, cliCtx, name, stdTx, appendSig, offline)
}
if err != nil {
return err
}

View File

@ -36,7 +36,7 @@ func SignTxRequestHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.Ha
}
// validate tx
// discard error if it's CodeNoSignatures as the tx comes with no signatures
// discard error if it's CodeNoSignatures as the tx comes with no signatures
if err := m.Tx.ValidateBasic(); err != nil && err.Code() != sdk.CodeNoSignatures {
utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return

View File

@ -3,9 +3,10 @@ package gov
import (
"testing"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/x/mock"
abci "github.com/tendermint/tendermint/abci/types"
)