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:
parent
eff1f7ca10
commit
26cb0a125a
@ -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.
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
159
x/auth/client/cli/multisign.go
Normal file
159
x/auth/client/cli/multisign.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user