Skip to content

Commit 5db395b

Browse files
mergify[bot]facundomedicatac0turtle
authored
fix: nested multisig signatures using CLI (backport #20438) (#20692)
Co-authored-by: Facundo Medica <[email protected]> Co-authored-by: marbar3778 <[email protected]> Co-authored-by: Facundo <[email protected]>
1 parent d6428f7 commit 5db395b

File tree

8 files changed

+137
-34
lines changed

8 files changed

+137
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
4646

4747
* (x/authz,x/feegrant) [#20590](https://github.com/cosmos/cosmos-sdk/pull/20590) Provide updated keeper in depinject for authz and feegrant modules.
4848
* [#20631](https://github.com/cosmos/cosmos-sdk/pull/20631) Fix json parsing in the wait-tx command.
49+
* (x/auth) [#20438](https://github.com/cosmos/cosmos-sdk/pull/20438) Add `--skip-signature-verification` flag to multisign command to allow nested multisigs.
4950

5051
## [v0.50.7](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.50.7) - 2024-06-04
5152

client/keys/output_test.go

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

47+
// TestBech32KeysOutputNestedMsig tests that the output of a nested multisig key is correct
48+
func TestBech32KeysOutputNestedMsig(t *testing.T) {
49+
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}}
50+
tmpKey := sk.PubKey()
51+
nestedMultiSig := kmultisig.NewLegacyAminoPubKey(1, []types.PubKey{tmpKey})
52+
multisigPk := kmultisig.NewLegacyAminoPubKey(2, []types.PubKey{tmpKey, nestedMultiSig})
53+
k, err := keyring.NewMultiRecord("multisig", multisigPk)
54+
require.NotNil(t, k)
55+
require.NoError(t, err)
56+
57+
pubKey, err := k.GetPubKey()
58+
require.NoError(t, err)
59+
60+
accAddr := sdk.AccAddress(pubKey.Address())
61+
expectedOutput, err := NewKeyOutput(k.Name, k.GetType(), accAddr, multisigPk)
62+
require.NoError(t, err)
63+
64+
out, err := MkAccKeyOutput(k)
65+
require.NoError(t, err)
66+
67+
require.Equal(t, expectedOutput, out)
68+
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))
69+
}
70+
4771
func TestProtoMarshalJSON(t *testing.T) {
4872
require := require.New(t)
4973
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 multisign 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
@@ -48,6 +48,10 @@ If the --offline flag is on, the client will not reach out to an external node.
4848
Account number or sequence number lookups are not performed so you must
4949
set these parameters manually.
5050
51+
If the --skip-signature-verification flag is on, the command will not verify the
52+
signatures in the provided signature files. This is useful when the multisig
53+
account is a signer in a nested multisig scenario.
54+
5155
The current multisig implementation defaults to amino-json sign mode.
5256
The SIGN_MODE_DIRECT sign mode is not supported.'
5357
`,
@@ -58,6 +62,7 @@ The SIGN_MODE_DIRECT sign mode is not supported.'
5862
Args: cobra.MinimumNArgs(3),
5963
}
6064

65+
cmd.Flags().Bool(flagSkipSignatureVerification, false, "Skip signature verification")
6166
cmd.Flags().Bool(flagSigOnly, false, "Print only the generated signature, then exit")
6267
cmd.Flags().String(flags.FlagOutputDocument, "", "The document is written to the given file instead of STDOUT")
6368
flags.AddTxFlagsToCmd(cmd)
@@ -105,6 +110,10 @@ func makeMultiSignCmd() func(cmd *cobra.Command, args []string) (err error) {
105110
return err
106111
}
107112

113+
// avoid signature verification if the sender of the tx is different than
114+
// the multisig key (useful for nested multisigs).
115+
skipSigVerify, _ := cmd.Flags().GetBool(flagSkipSignatureVerification)
116+
108117
multisigPub := pubKey.(*kmultisig.LegacyAminoPubKey)
109118
multisigSig := multisig.NewMultisig(len(multisigPub.PubKeys))
110119
if !clientCtx.Offline {
@@ -149,11 +158,13 @@ func makeMultiSignCmd() func(cmd *cobra.Command, args []string) (err error) {
149158
}
150159
txData := adaptableTx.GetSigningTxData()
151160

152-
err = signing.VerifySignature(cmd.Context(), sig.PubKey, txSignerData, sig.Data,
153-
txCfg.SignModeHandler(), txData)
154-
if err != nil {
155-
addr, _ := sdk.AccAddressFromHexUnsafe(sig.PubKey.Address().String())
156-
return fmt.Errorf("couldn't verify signature for address %s", addr)
161+
if !skipSigVerify {
162+
err = signing.VerifySignature(cmd.Context(), sig.PubKey, txSignerData, sig.Data,
163+
txCfg.SignModeHandler(), txData)
164+
if err != nil {
165+
addr, _ := sdk.AccAddressFromHexUnsafe(sig.PubKey.Address().String())
166+
return fmt.Errorf("couldn't verify signature for address %s %w", addr, err)
167+
}
157168
}
158169

159170
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
@@ -10,16 +10,18 @@ import (
1010
"github.com/cosmos/cosmos-sdk/client/flags"
1111
"github.com/cosmos/cosmos-sdk/client/tx"
1212
kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
13+
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
1314
sdk "github.com/cosmos/cosmos-sdk/types"
1415
authclient "github.com/cosmos/cosmos-sdk/x/auth/client"
1516
)
1617

1718
const (
18-
flagMultisig = "multisig"
19-
flagOverwrite = "overwrite"
20-
flagSigOnly = "signature-only"
21-
flagNoAutoIncrement = "no-auto-increment"
22-
flagAppend = "append"
19+
flagMultisig = "multisig"
20+
flagOverwrite = "overwrite"
21+
flagSigOnly = "signature-only"
22+
flagSkipSignatureVerification = "skip-signature-verification"
23+
flagNoAutoIncrement = "no-auto-increment"
24+
flagAppend = "append"
2325
)
2426

2527
// GetSignBatchCommand returns the transaction sign-batch command.
@@ -228,11 +230,40 @@ func sign(clientCtx client.Context, txBuilder client.TxBuilder, txFactory tx.Fac
228230
}
229231

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

238+
fromRecord, err := clientCtx.Keyring.Key(clientCtx.FromName)
239+
if err != nil {
240+
return fmt.Errorf("error getting account from keybase: %w", err)
241+
}
242+
243+
fromPubKey, err := fromRecord.GetPubKey()
244+
if err != nil {
245+
return err
246+
}
247+
248+
multisigkey, err := clientCtx.Keyring.Key(multisigName)
249+
if err != nil {
250+
return err
251+
}
252+
253+
multisigPubKey, err := multisigkey.GetPubKey()
254+
if err != nil {
255+
return err
256+
}
257+
258+
isSigner, err := isMultisigSigner(clientCtx, multisigPubKey, fromPubKey)
259+
if err != nil {
260+
return err
261+
}
262+
263+
if !isSigner {
264+
return fmt.Errorf("signing key is not a part of multisig key")
265+
}
266+
236267
if err = authclient.SignTxWithSignerAddress(
237268
txFactory,
238269
clientCtx,
@@ -248,6 +279,33 @@ func multisigSign(clientCtx client.Context, txBuilder client.TxBuilder, txFactor
248279
return nil
249280
}
250281

282+
// isMultisigSigner checks if the given pubkey is a signer in the multisig or in
283+
// any of the nested multisig signers.
284+
func isMultisigSigner(clientCtx client.Context, multisigPubKey, fromPubKey cryptotypes.PubKey) (bool, error) {
285+
multisigLegacyPub := multisigPubKey.(*kmultisig.LegacyAminoPubKey)
286+
287+
var found bool
288+
for _, pubkey := range multisigLegacyPub.GetPubKeys() {
289+
if pubkey.Equals(fromPubKey) {
290+
found = true
291+
break
292+
}
293+
294+
if nestedMultisig, ok := pubkey.(*kmultisig.LegacyAminoPubKey); ok {
295+
var err error
296+
found, err = isMultisigSigner(clientCtx, nestedMultisig, fromPubKey)
297+
if err != nil {
298+
return false, err
299+
}
300+
if found {
301+
break
302+
}
303+
}
304+
}
305+
306+
return found, nil
307+
}
308+
251309
func setOutputFile(cmd *cobra.Command) (func(), error) {
252310
outputDoc, _ := cmd.Flags().GetString(flags.FlagOutputDocument)
253311
if outputDoc == "" {
@@ -373,7 +431,6 @@ func signTx(cmd *cobra.Command, clientCtx client.Context, txF tx.Factory, newTx
373431
if err != nil {
374432
return err
375433
}
376-
multisigLegacyPub := multisigPubKey.(*kmultisig.LegacyAminoPubKey)
377434

378435
fromRecord, err := clientCtx.Keyring.Key(fromName)
379436
if err != nil {
@@ -384,15 +441,14 @@ func signTx(cmd *cobra.Command, clientCtx client.Context, txF tx.Factory, newTx
384441
return err
385442
}
386443

387-
var found bool
388-
for _, pubkey := range multisigLegacyPub.GetPubKeys() {
389-
if pubkey.Equals(fromPubKey) {
390-
found = true
391-
}
444+
isSigner, err := isMultisigSigner(clientCtx, multisigPubKey, fromPubKey)
445+
if err != nil {
446+
return err
392447
}
393-
if !found {
448+
if !isSigner {
394449
return fmt.Errorf("signing key is not a part of multisig key")
395450
}
451+
396452
err = authclient.SignTxWithSignerAddress(
397453
txF, clientCtx, multisigAddr, fromName, txBuilder, clientCtx.Offline, overwrite)
398454
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("%s: %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: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,18 @@ Ref: https://keepachangelog.com/en/1.0.0/
5656
### Improvements
5757

5858
* [#15871](https://github.com/cosmos/cosmos-sdk/pull/15871)
59-
* `HandlerMap` now has a `DefaultMode()` getter method
60-
* Textual types use `signing.ProtoFileResolver` instead of `protoregistry.Files`
59+
* `HandlerMap` now has a `DefaultMode()` getter method
60+
* Textual types use `signing.ProtoFileResolver` instead of `protoregistry.Files`
6161

6262
## v0.6.0
6363

6464
### API Breaking
6565

6666
* [#15709](https://github.com/cosmos/cosmos-sdk/pull/15709):
67-
* `GetSignersContext` has been renamed to `signing.Context`
68-
* `GetSigners` now returns `[][]byte` instead of `[]string`
69-
* `GetSignersOptions` has been renamed to `signing.Options` and requires `address.Codec`s for account and validator addresses
70-
* `GetSignersOptions.ProtoFiles` has been renamed to `signing.Options.FileResolver`
67+
* `GetSignersContext` has been renamed to `signing.Context`
68+
* `GetSigners` now returns `[][]byte` instead of `[]string`
69+
* `GetSignersOptions` has been renamed to `signing.Options` and requires `address.Codec`s for account and validator addresses
70+
* `GetSignersOptions.ProtoFiles` has been renamed to `signing.Options.FileResolver`
7171

7272
### Bug Fixes
7373

0 commit comments

Comments
 (0)