Skip to content

Commit b9ca318

Browse files
fix: nested multisig signatures using CLI (#20438)
1 parent 32bdca3 commit b9ca318

File tree

7 files changed

+134
-28
lines changed

7 files changed

+134
-28
lines changed

client/keys/output_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,30 @@ func TestBech32KeysOutput(t *testing.T) {
4545
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))
4646
}
4747

48+
// TestBech32KeysOutputNestedMsig tests that the output of a nested multisig key is correct
49+
func TestBech32KeysOutputNestedMsig(t *testing.T) {
50+
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}}
51+
tmpKey := sk.PubKey()
52+
nestedMultiSig := kmultisig.NewLegacyAminoPubKey(1, []types.PubKey{tmpKey})
53+
multisigPk := kmultisig.NewLegacyAminoPubKey(2, []types.PubKey{tmpKey, nestedMultiSig})
54+
k, err := keyring.NewMultiRecord("multisig", multisigPk)
55+
require.NotNil(t, k)
56+
require.NoError(t, err)
57+
58+
pubKey, err := k.GetPubKey()
59+
require.NoError(t, err)
60+
61+
accAddr := sdk.AccAddress(pubKey.Address())
62+
expectedOutput, err := NewKeyOutput(k.Name, k.GetType(), accAddr, multisigPk, addresscodec.NewBech32Codec("cosmos"))
63+
require.NoError(t, err)
64+
65+
out, err := MkAccKeyOutput(k, addresscodec.NewBech32Codec("cosmos"))
66+
require.NoError(t, err)
67+
68+
require.Equal(t, expectedOutput, out)
69+
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))
70+
}
71+
4872
func TestProtoMarshalJSON(t *testing.T) {
4973
require := require.New(t)
5074
pubkeys := generatePubKeys(3)

crypto/keys/multisig/multisig.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ func packPubKeys(pubKeys []cryptotypes.PubKey) ([]*types.Any, error) {
169169
return nil, err
170170
}
171171
anyPubKeys[i] = any
172+
173+
// sets the compat.aminoBz value
174+
if err := anyPubKeys[i].UnmarshalAmino(pubKeys[i].Bytes()); err != nil {
175+
return nil, err
176+
}
172177
}
173178
return anyPubKeys, nil
174179
}

x/auth/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,22 @@ simd tx multi-sign transaction.json k1k2k3 k1sig.json k2sig.json k3sig.json
447447

448448
Where `k1k2k3` is the multisig account address, `k1sig.json` is the signature of the first signer, `k2sig.json` is the signature of the second signer, and `k3sig.json` is the signature of the third signer.
449449

450+
##### Nested multisig transactions
451+
452+
To allow transactions to be signed by nested multisigs, meaning that a participant of a multisig account can be another multisig account, the `--skip-signature-verification` flag must be used.
453+
454+
```bash
455+
# First aggregate signatures of the multisig participant
456+
simd tx multi-sign transaction.json ms1 ms1p1sig.json ms1p2sig.json --signature-only --skip-signature-verification > ms1sig.json
457+
458+
# Then use the aggregated signatures and the other signatures to sign the final transaction
459+
simd tx multi-sign transaction.json k1ms1 k1sig.json ms1sig.json --skip-signature-verification
460+
```
461+
462+
Where `ms1` is the nested multisig account address, `ms1p1sig.json` is the signature of the first participant of the nested multisig account, `ms1p2sig.json` is the signature of the second participant of the nested multisig account, and `ms1sig.json` is the aggregated signature of the nested multisig account.
463+
464+
`k1ms1` is a multisig account comprised of an individual signer and another nested multisig account (`ms1`). `k1sig.json` is the signature of the first signer of the individual member.
465+
450466
More information about the `multi-sign` command can be found running `simd tx multi-sign --help`.
451467

452468
#### `multisign-batch`

x/auth/client/cli/tx_multisign.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ If the --offline flag is on, the client will not reach out to an external node.
4747
Account number or sequence number lookups are not performed so you must
4848
set these parameters manually.
4949
50+
If the --skip-signature-verification flag is on, the command will not verify the
51+
signatures in the provided signature files. This is useful when the multisig
52+
account is a signer in a nested multisig scenario.
53+
5054
The current multisig implementation defaults to amino-json sign mode.
5155
The SIGN_MODE_DIRECT sign mode is not supported.'
5256
`,
@@ -57,6 +61,7 @@ The SIGN_MODE_DIRECT sign mode is not supported.'
5761
Args: cobra.MinimumNArgs(3),
5862
}
5963

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

117+
// avoid signature verification if the sender of the tx is different than
118+
// the multisig key (useful for nested multisigs).
119+
skipSigVerify, _ := cmd.Flags().GetBool(flagSkipSignatureVerification)
120+
112121
multisigPub := pubKey.(*kmultisig.LegacyAminoPubKey)
113122
multisigSig := multisig.NewMultisig(len(multisigPub.PubKeys))
114123
if !clientCtx.Offline {
@@ -153,11 +162,13 @@ func makeMultiSignCmd() func(cmd *cobra.Command, args []string) (err error) {
153162
}
154163
txData := adaptableTx.GetSigningTxData()
155164

156-
err = signing.VerifySignature(cmd.Context(), sig.PubKey, txSignerData, sig.Data,
157-
txCfg.SignModeHandler(), txData)
158-
if err != nil {
159-
addr, _ := sdk.AccAddressFromHexUnsafe(sig.PubKey.Address().String())
160-
return fmt.Errorf("couldn't verify signature for address %s", addr)
165+
if !skipSigVerify {
166+
err = signing.VerifySignature(cmd.Context(), sig.PubKey, txSignerData, sig.Data,
167+
txCfg.SignModeHandler(), txData)
168+
if err != nil {
169+
addr, _ := sdk.AccAddressFromHexUnsafe(sig.PubKey.Address().String())
170+
return fmt.Errorf("couldn't verify signature for address %s %w", addr, err)
171+
}
161172
}
162173

163174
if err := multisig.AddSignatureV2(multisigSig, sig, multisigPub.GetPubKeys()); err != nil {

x/auth/client/cli/tx_sign.go

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ import (
1414
"github.com/cosmos/cosmos-sdk/client/flags"
1515
"github.com/cosmos/cosmos-sdk/client/tx"
1616
kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
17+
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
1718
sdk "github.com/cosmos/cosmos-sdk/types"
1819
)
1920

2021
const (
21-
flagMultisig = "multisig"
22-
flagOverwrite = "overwrite"
23-
flagSigOnly = "signature-only"
24-
flagNoAutoIncrement = "no-auto-increment"
25-
flagAppend = "append"
22+
flagMultisig = "multisig"
23+
flagOverwrite = "overwrite"
24+
flagSigOnly = "signature-only"
25+
flagSkipSignatureVerification = "skip-signature-verification"
26+
flagNoAutoIncrement = "no-auto-increment"
27+
flagAppend = "append"
2628
)
2729

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

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

234+
fromRecord, err := clientCtx.Keyring.Key(clientCtx.FromName)
235+
if err != nil {
236+
return fmt.Errorf("error getting account from keybase: %w", err)
237+
}
238+
239+
fromPubKey, err := fromRecord.GetPubKey()
240+
if err != nil {
241+
return err
242+
}
243+
244+
multisigkey, err := clientCtx.Keyring.Key(multisigName)
245+
if err != nil {
246+
return err
247+
}
248+
249+
multisigPubKey, err := multisigkey.GetPubKey()
250+
if err != nil {
251+
return err
252+
}
253+
254+
isSigner, err := isMultisigSigner(clientCtx, multisigPubKey, fromPubKey)
255+
if err != nil {
256+
return err
257+
}
258+
259+
if !isSigner {
260+
return fmt.Errorf("signing key is not a part of multisig key")
261+
}
262+
232263
if err = authclient.SignTxWithSignerAddress(
233264
txFactory,
234265
clientCtx,
@@ -244,6 +275,33 @@ func multisigSign(clientCtx client.Context, txBuilder client.TxBuilder, txFactor
244275
return nil
245276
}
246277

278+
// isMultisigSigner checks if the given pubkey is a signer in the multisig or in
279+
// any of the nested multisig signers.
280+
func isMultisigSigner(clientCtx client.Context, multisigPubKey, fromPubKey cryptotypes.PubKey) (bool, error) {
281+
multisigLegacyPub := multisigPubKey.(*kmultisig.LegacyAminoPubKey)
282+
283+
var found bool
284+
for _, pubkey := range multisigLegacyPub.GetPubKeys() {
285+
if pubkey.Equals(fromPubKey) {
286+
found = true
287+
break
288+
}
289+
290+
if nestedMultisig, ok := pubkey.(*kmultisig.LegacyAminoPubKey); ok {
291+
var err error
292+
found, err = isMultisigSigner(clientCtx, nestedMultisig, fromPubKey)
293+
if err != nil {
294+
return false, err
295+
}
296+
if found {
297+
break
298+
}
299+
}
300+
}
301+
302+
return found, nil
303+
}
304+
247305
func setOutputFile(cmd *cobra.Command) (func(), error) {
248306
outputDoc, _ := cmd.Flags().GetString(flags.FlagOutputDocument)
249307
if outputDoc == "" {
@@ -376,7 +434,6 @@ func signTx(cmd *cobra.Command, clientCtx client.Context, txFactory tx.Factory,
376434
if err != nil {
377435
return err
378436
}
379-
multisigLegacyPub := multisigPubKey.(*kmultisig.LegacyAminoPubKey)
380437

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

390-
var found bool
391-
for _, pubkey := range multisigLegacyPub.GetPubKeys() {
392-
if pubkey.Equals(fromPubKey) {
393-
found = true
394-
}
447+
isSigner, err := isMultisigSigner(clientCtx, multisigPubKey, fromPubKey)
448+
if err != nil {
449+
return err
395450
}
396-
if !found {
451+
if !isSigner {
397452
return fmt.Errorf("signing key is not a part of multisig key")
398453
}
454+
399455
err = authclient.SignTxWithSignerAddress(
400456
txFactory, clientCtx, multisigAddr, fromName, txBuilder, clientCtx.Offline, overwrite)
401457
if err != nil {

x/auth/client/tx.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,6 @@ func SignTxWithSignerAddress(txFactory tx.Factory, clientCtx client.Context, add
7878
txFactory = txFactory.WithSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON)
7979
}
8080

81-
// check whether the address is a signer
82-
signers, err := txBuilder.GetTx().GetSigners()
83-
if err != nil {
84-
return err
85-
}
86-
87-
if !isTxSigner(addr, signers) {
88-
return fmt.Errorf("%w: %s", errors.ErrorInvalidSigner, name)
89-
}
90-
9181
if !offline {
9282
txFactory, err = populateAccountFromState(txFactory, clientCtx, addr)
9383
if err != nil {

x/tx/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ Since v0.13.0, x/tx follows Cosmos SDK semver: https://github.com/cosmos/cosmos-
3333

3434
## [Unreleased]
3535

36+
### Improvements
37+
38+
* [#20438](https://github.com/cosmos/cosmos-sdk/pull/20438) Add `--skip-signature-verification` flag to multisign command to allow nested multisigs.
39+
3640
## [v0.13.3](https://github.com/cosmos/cosmos-sdk/releases/tag/x/tx/v0.13.3) - 2024-04-22
3741

3842
### Improvements

0 commit comments

Comments
 (0)