diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index d088b841fc45..acccd1fbf6fe 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -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" diff --git a/client/keys/add.go b/client/keys/add.go index 2a0890fbe65d..b3af588dc8b4 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -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 } diff --git a/client/utils/utils.go b/client/utils/utils.go index 98c53ee5ca8e..92ce82b9937f 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -133,20 +133,40 @@ 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) @@ -154,7 +174,28 @@ func SignStdTx(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, name string, 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. diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index c77ed5a2ed4e..8f69c41b0cc6 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -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) @@ -646,6 +666,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) diff --git a/cmd/gaia/cli_test/test_helpers.go b/cmd/gaia/cli_test/test_helpers.go index db4d3544e8a2..5e9e5320a43d 100644 --- a/cmd/gaia/cli_test/test_helpers.go +++ b/cmd/gaia/cli_test/test_helpers.go @@ -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 diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index 176be4f97f92..923be187b0d1 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -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, ) diff --git a/cmd/gaia/cmd/gaiad/main.go b/cmd/gaia/cmd/gaiad/main.go index 6d597eed7089..14dd603a914d 100644 --- a/cmd/gaia/cmd/gaiad/main.go +++ b/cmd/gaia/cmd/gaiad/main.go @@ -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" diff --git a/cmd/gaia/cmd/gaiareplay/main.go b/cmd/gaia/cmd/gaiareplay/main.go index cf946edce3c1..ef6bcb139a4a 100644 --- a/cmd/gaia/cmd/gaiareplay/main.go +++ b/cmd/gaia/cmd/gaiareplay/main.go @@ -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" diff --git a/cmd/gaia/init/genesis_accts.go b/cmd/gaia/init/genesis_accts.go index 4f5043877956..bb9c4af0c8ce 100644 --- a/cmd/gaia/init/genesis_accts.go +++ b/cmd/gaia/init/genesis_accts.go @@ -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 } diff --git a/docs/examples/democoin/x/simplestaking/client/cli/commands.go b/docs/examples/democoin/x/simplestaking/client/cli/commands.go index 5a2b819f3f62..37b1c20ded7d 100644 --- a/docs/examples/democoin/x/simplestaking/client/cli/commands.go +++ b/docs/examples/democoin/x/simplestaking/client/cli/commands.go @@ -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" diff --git a/docs/gaia/gaiacli.md b/docs/gaia/gaiacli.md index 7e7274ef9653..82cd55b0551b 100644 --- a/docs/gaia/gaiacli.md +++ b/docs/gaia/gaiacli.md @@ -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= \ + --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= \ + --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= \ + --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 +``` diff --git a/x/auth/ante.go b/x/auth/ante.go index c25826266b27..2e84644b3441 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -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)) diff --git a/x/auth/ante_test.go b/x/auth/ante_test.go index 9511798a57bd..e8e435ffd2f3 100644 --- a/x/auth/ante_test.go +++ b/x/auth/ante_test.go @@ -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 diff --git a/x/auth/client/cli/multisign.go b/x/auth/client/cli/multisign.go new file mode 100644 index 000000000000..9fcc8a0a0730 --- /dev/null +++ b/x/auth/client/cli/multisign.go @@ -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 <...>", + 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 file(s), generate a multisig signature compliant to the +multisig key , and attach it to the transaction read from . 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 +} diff --git a/x/auth/client/cli/sign.go b/x/auth/client/cli/sign.go index 94c2aa932568..1e0bc895ff0a 100644 --- a/x/auth/client/cli/sign.go +++ b/x/auth/client/cli/sign.go @@ -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= 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 } diff --git a/x/auth/client/rest/sign.go b/x/auth/client/rest/sign.go index 754a07b44e60..a104c7fa14cf 100644 --- a/x/auth/client/rest/sign.go +++ b/x/auth/client/rest/sign.go @@ -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 diff --git a/x/gov/genesis_test.go b/x/gov/genesis_test.go index ab3810068520..87d466b5f179 100644 --- a/x/gov/genesis_test.go +++ b/x/gov/genesis_test.go @@ -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" )