Skip to content
24 changes: 24 additions & 0 deletions client/keys/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,30 @@ func TestBech32KeysOutput(t *testing.T) {
require.Equal(t, "{Name:multisig Type:multi Address:cosmos1nf8lf6n4wa43rzmdzwe6hkrnw5guekhqt595cw PubKey:{\"@type\":\"/cosmos.crypto.multisig.LegacyAminoPubKey\",\"threshold\":1,\"public_keys\":[{\"@type\":\"/cosmos.crypto.secp256k1.PubKey\",\"key\":\"AurroA7jvfPd1AadmmOvWM2rJSwipXfRf8yD6pLbA2DJ\"}]} Mnemonic:}", fmt.Sprintf("%+v", out))
}

// TestBech32KeysOutputNestedMsig tests that the output of a nested multisig key is correct
func TestBech32KeysOutputNestedMsig(t *testing.T) {
sk := secp256k1.PrivKey{Key: []byte{154, 49, 3, 117, 55, 232, 249, 20, 205, 216, 102, 7, 136, 72, 177, 2, 131, 202, 234, 81, 31, 208, 46, 244, 179, 192, 167, 163, 142, 117, 246, 13}}
tmpKey := sk.PubKey()
nestedMultiSig := kmultisig.NewLegacyAminoPubKey(1, []types.PubKey{tmpKey})
multisigPk := kmultisig.NewLegacyAminoPubKey(2, []types.PubKey{tmpKey, nestedMultiSig})
k, err := keyring.NewMultiRecord("multisig", multisigPk)
require.NotNil(t, k)
require.NoError(t, err)

pubKey, err := k.GetPubKey()
require.NoError(t, err)

accAddr := sdk.AccAddress(pubKey.Address())
expectedOutput, err := NewKeyOutput(k.Name, k.GetType(), accAddr, multisigPk, addresscodec.NewBech32Codec("cosmos"))
require.NoError(t, err)

out, err := MkAccKeyOutput(k, addresscodec.NewBech32Codec("cosmos"))
require.NoError(t, err)

require.Equal(t, expectedOutput, out)
require.Equal(t, "{Name:multisig Type:multi Address:cosmos1nffp6v2j7wva4y4975exlrv8x5vh39axxt3swz PubKey:{\"@type\":\"/cosmos.crypto.multisig.LegacyAminoPubKey\",\"threshold\":2,\"public_keys\":[{\"@type\":\"/cosmos.crypto.secp256k1.PubKey\",\"key\":\"AurroA7jvfPd1AadmmOvWM2rJSwipXfRf8yD6pLbA2DJ\"},{\"@type\":\"/cosmos.crypto.multisig.LegacyAminoPubKey\",\"threshold\":1,\"public_keys\":[{\"@type\":\"/cosmos.crypto.secp256k1.PubKey\",\"key\":\"AurroA7jvfPd1AadmmOvWM2rJSwipXfRf8yD6pLbA2DJ\"}]}]} Mnemonic:}", fmt.Sprintf("%+v", out))
}
Comment on lines +48 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test TestBech32KeysOutputNestedMsig appears comprehensive and correctly tests the output of a nested multisig key. However, consider adding more assertions to verify the structure of the nested multisig key in more detail, such as checking individual public keys within the nested structure.


func TestProtoMarshalJSON(t *testing.T) {
require := require.New(t)
pubkeys := generatePubKeys(3)
Expand Down
3 changes: 3 additions & 0 deletions crypto/keys/multisig/multisig.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ func packPubKeys(pubKeys []cryptotypes.PubKey) ([]*types.Any, error) {
return nil, err
}
anyPubKeys[i] = any

// sets the compat.aminoBz value
anyPubKeys[i].UnmarshalAmino(pubKeys[i].Bytes())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the necessary fix to allow nested anys

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error return value of UnmarshalAmino is not checked. It's important to handle potential errors to avoid runtime panics or unexpected behavior.

- anyPubKeys[i].UnmarshalAmino(pubKeys[i].Bytes())
+ if err := anyPubKeys[i].UnmarshalAmino(pubKeys[i].Bytes()); err != nil {
+     return nil, err
+ }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
anyPubKeys[i].UnmarshalAmino(pubKeys[i].Bytes())
if err := anyPubKeys[i].UnmarshalAmino(pubKeys[i].Bytes()); err != nil {
return nil, err
}
Tools
golangci-lint

174-174: Error return value of (*github.com/cosmos/gogoproto/types/any.Any).UnmarshalAmino is not checked (errcheck)

GitHub Check: golangci-lint

[failure] 174-174:
Error return value of (*github.com/cosmos/gogoproto/types/any.Any).UnmarshalAmino is not checked (errcheck)

Comment thread Fixed
}
return anyPubKeys, nil
}
21 changes: 16 additions & 5 deletions x/auth/client/cli/tx_multisign.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ If the --offline flag is on, the client will not reach out to an external node.
Account number or sequence number lookups are not performed so you must
set these parameters manually.

If the --skip-signature-verification flag is on, the command will not verify the
signatures in the provided signature files. This is useful when the multisig
account is a signer in a nested multisig scenario.

The current multisig implementation defaults to amino-json sign mode.
The SIGN_MODE_DIRECT sign mode is not supported.'
`,
Expand All @@ -57,6 +61,7 @@ The SIGN_MODE_DIRECT sign mode is not supported.'
Args: cobra.MinimumNArgs(3),
}

cmd.Flags().Bool(flagSkipSignatureVerification, false, "Skip signature verification")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add this to some docs somewhere? easy to get lost in the immense amount of code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added docs and changelog 💪

cmd.Flags().Bool(flagSigOnly, false, "Print only the generated signature, then exit")
cmd.Flags().String(flags.FlagOutputDocument, "", "The document is written to the given file instead of STDOUT")
flags.AddTxFlagsToCmd(cmd)
Expand Down Expand Up @@ -109,6 +114,10 @@ func makeMultiSignCmd() func(cmd *cobra.Command, args []string) (err error) {
return err
}

// avoid signature verification if the sender of the tx is different than
// the multisig key (useful for nested multisigs).
skipSigVerify, _ := cmd.Flags().GetBool(flagSkipSignatureVerification)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should describe this flag somewhere in the docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!


multisigPub := pubKey.(*kmultisig.LegacyAminoPubKey)
multisigSig := multisig.NewMultisig(len(multisigPub.PubKeys))
if !clientCtx.Offline {
Expand Down Expand Up @@ -153,11 +162,13 @@ func makeMultiSignCmd() func(cmd *cobra.Command, args []string) (err error) {
}
txData := adaptableTx.GetSigningTxData()

err = signing.VerifySignature(cmd.Context(), sig.PubKey, txSignerData, sig.Data,
txCfg.SignModeHandler(), txData)
if err != nil {
addr, _ := sdk.AccAddressFromHexUnsafe(sig.PubKey.Address().String())
return fmt.Errorf("couldn't verify signature for address %s", addr)
if !skipSigVerify {
err = signing.VerifySignature(cmd.Context(), sig.PubKey, txSignerData, sig.Data,
txCfg.SignModeHandler(), txData)
if err != nil {
addr, _ := sdk.AccAddressFromHexUnsafe(sig.PubKey.Address().String())
return fmt.Errorf("couldn't verify signature for address %s %w", addr, err)
}
}

if err := multisig.AddSignatureV2(multisigSig, sig, multisigPub.GetPubKeys()); err != nil {
Expand Down
82 changes: 69 additions & 13 deletions x/auth/client/cli/tx_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ import (
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

const (
flagMultisig = "multisig"
flagOverwrite = "overwrite"
flagSigOnly = "signature-only"
flagNoAutoIncrement = "no-auto-increment"
flagAppend = "append"
flagMultisig = "multisig"
flagOverwrite = "overwrite"
flagSigOnly = "signature-only"
flagSkipSignatureVerification = "skip-signature-verification"
flagNoAutoIncrement = "no-auto-increment"
flagAppend = "append"
)

// GetSignBatchCommand returns the transaction sign-batch command.
Expand Down Expand Up @@ -224,11 +226,40 @@ func sign(clientCtx client.Context, txBuilder client.TxBuilder, txFactory tx.Fac
}

func multisigSign(clientCtx client.Context, txBuilder client.TxBuilder, txFactory tx.Factory, multisig string) error {
multisigAddr, _, _, err := client.GetFromFields(clientCtx, txFactory.Keybase(), multisig)
multisigAddr, multisigName, _, err := client.GetFromFields(clientCtx, txFactory.Keybase(), multisig)
if err != nil {
return fmt.Errorf("error getting account from keybase: %w", err)
}

fromRecord, err := clientCtx.Keyring.Key(clientCtx.FromName)
if err != nil {
return fmt.Errorf("error getting account from keybase: %w", err)
}

fromPubKey, err := fromRecord.GetPubKey()
if err != nil {
return err
}

multisigkey, err := clientCtx.Keyring.Key(multisigName)
if err != nil {
return err
}

multisigPubKey, err := multisigkey.GetPubKey()
if err != nil {
return err
}

isSigner, err := isMultisigSigner(clientCtx, multisigPubKey, fromPubKey)
if err != nil {
return err
}

if !isSigner {
return fmt.Errorf("signing key is not a part of multisig key")
}

if err = authclient.SignTxWithSignerAddress(
txFactory,
clientCtx,
Expand All @@ -244,6 +275,33 @@ func multisigSign(clientCtx client.Context, txBuilder client.TxBuilder, txFactor
return nil
}

// isMultisigSigner checks if the given pubkey is a signer in the multisig or in
// any of the nested multisig signers.
func isMultisigSigner(clientCtx client.Context, multisigPubKey, fromPubKey cryptotypes.PubKey) (bool, error) {
multisigLegacyPub := multisigPubKey.(*kmultisig.LegacyAminoPubKey)

var found bool
for _, pubkey := range multisigLegacyPub.GetPubKeys() {
if pubkey.Equals(fromPubKey) {
found = true
break
}

if nestedMultisig, ok := pubkey.(*kmultisig.LegacyAminoPubKey); ok {
var err error
found, err = isMultisigSigner(clientCtx, nestedMultisig, fromPubKey)
if err != nil {
return false, err
}
if found {
break
}
}
}

return found, nil
}

func setOutputFile(cmd *cobra.Command) (func(), error) {
outputDoc, _ := cmd.Flags().GetString(flags.FlagOutputDocument)
if outputDoc == "" {
Expand Down Expand Up @@ -376,7 +434,6 @@ func signTx(cmd *cobra.Command, clientCtx client.Context, txFactory tx.Factory,
if err != nil {
return err
}
multisigLegacyPub := multisigPubKey.(*kmultisig.LegacyAminoPubKey)

fromRecord, err := clientCtx.Keyring.Key(fromName)
if err != nil {
Expand All @@ -387,15 +444,14 @@ func signTx(cmd *cobra.Command, clientCtx client.Context, txFactory tx.Factory,
return err
}

var found bool
for _, pubkey := range multisigLegacyPub.GetPubKeys() {
if pubkey.Equals(fromPubKey) {
found = true
}
isSigner, err := isMultisigSigner(clientCtx, multisigPubKey, fromPubKey)
if err != nil {
return err
}
if !found {
if !isSigner {
return fmt.Errorf("signing key is not a part of multisig key")
}

err = authclient.SignTxWithSignerAddress(
txFactory, clientCtx, multisigAddr, fromName, txBuilder, clientCtx.Offline, overwrite)
if err != nil {
Expand Down
10 changes: 0 additions & 10 deletions x/auth/client/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,6 @@ func SignTxWithSignerAddress(txFactory tx.Factory, clientCtx client.Context, add
txFactory = txFactory.WithSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON)
}

// check whether the address is a signer
signers, err := txBuilder.GetTx().GetSigners()
if err != nil {
return err
}

if !isTxSigner(addr, signers) {
return fmt.Errorf("%w: %s", errors.ErrorInvalidSigner, name)
}

if !offline {
txFactory, err = populateAccountFromState(txFactory, clientCtx, addr)
if err != nil {
Expand Down