From 4a35885fb77ac0d4467c6869ccfc4c6971dee7df Mon Sep 17 00:00:00 2001 From: Matt Kocubinski Date: Thu, 16 Mar 2023 05:29:41 -0500 Subject: [PATCH] feat(x/tx): implement SIGN_MODE_DIRECT_AUX handler (#15380) Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com> --- x/tx/signing/direct_aux/direct_aux.go | 111 ++++++++++++++++ x/tx/signing/direct_aux/direct_aux_test.go | 144 +++++++++++++++++++++ x/tx/signing/get_signers.go | 3 +- 3 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 x/tx/signing/direct_aux/direct_aux.go create mode 100644 x/tx/signing/direct_aux/direct_aux_test.go diff --git a/x/tx/signing/direct_aux/direct_aux.go b/x/tx/signing/direct_aux/direct_aux.go new file mode 100644 index 0000000000..8526942049 --- /dev/null +++ b/x/tx/signing/direct_aux/direct_aux.go @@ -0,0 +1,111 @@ +package direct_aux + +import ( + "context" + "fmt" + + "github.com/cosmos/cosmos-proto/anyutil" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoregistry" + + signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" + "cosmossdk.io/x/tx/signing" +) + +// SignModeHandler is the SIGN_MODE_DIRECT_AUX implementation of signing.SignModeHandler. +type SignModeHandler struct { + signersContext *signing.GetSignersContext + fileResolver protodesc.Resolver + typeResolver protoregistry.MessageTypeResolver +} + +// SignModeHandlerOptions are the options for the SignModeHandler. +type SignModeHandlerOptions struct { + // FileResolver is the protodesc.Resolver to use for resolving proto files when unpacking any messages. + FileResolver protodesc.Resolver + + // TypeResolver is the protoregistry.MessageTypeResolver to use for resolving proto types when unpacking any messages. + TypeResolver protoregistry.MessageTypeResolver + + // SignersContext is the signing.GetSignersContext to use for getting signers. + SignersContext *signing.GetSignersContext +} + +// NewSignModeHandler returns a new SignModeHandler. +func NewSignModeHandler(options SignModeHandlerOptions) SignModeHandler { + h := SignModeHandler{} + + if options.FileResolver == nil { + h.fileResolver = protoregistry.GlobalFiles + } else { + h.fileResolver = options.FileResolver + } + + if options.TypeResolver == nil { + h.typeResolver = protoregistry.GlobalTypes + } else { + h.typeResolver = options.TypeResolver + } + + if options.SignersContext == nil { + h.signersContext = signing.NewGetSignersContext(signing.GetSignersOptions{ProtoFiles: h.fileResolver}) + } else { + h.signersContext = options.SignersContext + } + + return h +} + +var _ signing.SignModeHandler = SignModeHandler{} + +// Mode implements signing.SignModeHandler.Mode. +func (h SignModeHandler) Mode() signingv1beta1.SignMode { + return signingv1beta1.SignMode_SIGN_MODE_DIRECT_AUX +} + +// getFirstSigner returns the first signer from the first message in the tx. It replicates behavior in +// https://github.com/cosmos/cosmos-sdk/blob/4a6a1e3cb8de459891cb0495052589673d14ef51/x/auth/tx/builder.go#L142 +func (h SignModeHandler) getFirstSigner(txData signing.TxData) (string, error) { + for _, anyMsg := range txData.Body.Messages { + msg, err := anyutil.Unpack(anyMsg, h.fileResolver, h.typeResolver) + if err != nil { + return "", err + } + signer, err := h.signersContext.GetSigners(msg) + if err != nil { + return "", err + } + return signer[0], nil + } + return "", fmt.Errorf("no signer found") +} + +// GetSignBytes implements signing.SignModeHandler.GetSignBytes. +func (h SignModeHandler) GetSignBytes( + _ context.Context, signerData signing.SignerData, txData signing.TxData) ([]byte, error) { + + feePayer := txData.AuthInfo.Fee.Payer + if feePayer == "" { + fp, err := h.getFirstSigner(txData) + if err != nil { + return nil, err + } + feePayer = fp + } + if feePayer == signerData.Address { + return nil, fmt.Errorf("fee payer %s cannot sign with %s: unauthorized", + feePayer, signingv1beta1.SignMode_SIGN_MODE_DIRECT_AUX) + } + + signDocDirectAux := &txv1beta1.SignDocDirectAux{ + BodyBytes: txData.BodyBytes, + PublicKey: signerData.PubKey, + ChainId: signerData.ChainId, + AccountNumber: signerData.AccountNumber, + Sequence: signerData.Sequence, + Tip: txData.AuthInfo.Tip, + } + return proto.Marshal(signDocDirectAux) +} diff --git a/x/tx/signing/direct_aux/direct_aux_test.go b/x/tx/signing/direct_aux/direct_aux_test.go new file mode 100644 index 0000000000..45ef96e5a4 --- /dev/null +++ b/x/tx/signing/direct_aux/direct_aux_test.go @@ -0,0 +1,144 @@ +package direct_aux_test + +import ( + "context" + "fmt" + "testing" + + "github.com/cosmos/cosmos-proto/anyutil" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" + basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" + "cosmossdk.io/api/cosmos/crypto/secp256k1" + signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" + "cosmossdk.io/x/tx/signing" + "cosmossdk.io/x/tx/signing/direct_aux" +) + +func TestDirectAuxHandler(t *testing.T) { + feePayerAddr := "feePayer" + chainID := "test-chain" + memo := "sometestmemo" + msg, err := anyutil.New(&bankv1beta1.MsgSend{}) + require.NoError(t, err) + accNum, accSeq := uint64(1), uint64(2) // Arbitrary account number/sequence + + pk := &secp256k1.PubKey{ + Key: make([]byte, 256), + } + anyPk, err := anyutil.New(pk) + require.NoError(t, err) + + signerInfo := []*txv1beta1.SignerInfo{ + { + PublicKey: anyPk, + ModeInfo: &txv1beta1.ModeInfo{ + Sum: &txv1beta1.ModeInfo_Single_{ + Single: &txv1beta1.ModeInfo_Single{ + Mode: signingv1beta1.SignMode_SIGN_MODE_DIRECT_AUX, + }, + }, + }, + Sequence: accSeq, + }, + } + + fee := &txv1beta1.Fee{ + Amount: []*basev1beta1.Coin{{Denom: "uatom", Amount: "1000"}}, + GasLimit: 20000, + Payer: feePayerAddr, + } + tip := &txv1beta1.Tip{Amount: []*basev1beta1.Coin{{Denom: "tip-token", Amount: "10"}}} + + txBody := &txv1beta1.TxBody{ + Messages: []*anypb.Any{msg}, + Memo: memo, + } + + authInfo := &txv1beta1.AuthInfo{ + Fee: fee, + Tip: tip, + SignerInfos: signerInfo, + } + + signingData := signing.SignerData{ + ChainId: chainID, + AccountNumber: accNum, + Sequence: accSeq, + Address: "", + PubKey: anyPk, + } + + bodyBz, err := proto.Marshal(txBody) + require.NoError(t, err) + authInfoBz, err := proto.Marshal(authInfo) + require.NoError(t, err) + txData := signing.TxData{ + Body: txBody, + AuthInfo: authInfo, + AuthInfoBytes: authInfoBz, + BodyBytes: bodyBz, + } + modeHandler := direct_aux.NewSignModeHandler(direct_aux.SignModeHandlerOptions{}) + + t.Log("verify fee payer cannot use SIGN_MODE_DIRECT_AUX") + feePayerSigningData := signing.SignerData{ + ChainId: chainID, + AccountNumber: accNum, + Address: feePayerAddr, + PubKey: anyPk, + } + _, err = modeHandler.GetSignBytes(context.Background(), feePayerSigningData, txData) + require.EqualError(t, err, fmt.Sprintf("fee payer %s cannot sign with %s: unauthorized", + feePayerAddr, signingv1beta1.SignMode_SIGN_MODE_DIRECT_AUX)) + + t.Log("verifying fee payer fallback to GetSigners cannot use SIGN_MODE_DIRECT_AUX") + feeWithNoPayer := &txv1beta1.Fee{ + Amount: []*basev1beta1.Coin{{Denom: "uatom", Amount: "1000"}}, + GasLimit: 20000, + } + authInfoWithNoFeePayer := &txv1beta1.AuthInfo{ + Fee: feeWithNoPayer, + Tip: tip, + SignerInfos: signerInfo, + } + authInfoWithNoFeePayerBz, err := proto.Marshal(authInfoWithNoFeePayer) + require.NoError(t, err) + txDataWithNoFeePayer := signing.TxData{ + Body: txBody, + BodyBytes: bodyBz, + AuthInfo: authInfoWithNoFeePayer, + AuthInfoBytes: authInfoWithNoFeePayerBz, + } + _, err = modeHandler.GetSignBytes(context.Background(), signingData, txDataWithNoFeePayer) + require.EqualError(t, err, fmt.Sprintf("fee payer %s cannot sign with %s: unauthorized", "", + signingv1beta1.SignMode_SIGN_MODE_DIRECT_AUX)) + + t.Log("verify GetSignBytes with generating sign bytes by marshaling signDocDirectAux") + signBytes, err := modeHandler.GetSignBytes(context.Background(), signingData, txData) + require.NoError(t, err) + require.NotNil(t, signBytes) + + signDocDirectAux := &txv1beta1.SignDocDirectAux{ + BodyBytes: bodyBz, + PublicKey: anyPk, + ChainId: chainID, + AccountNumber: accNum, + Sequence: accSeq, + Tip: tip, + } + expectedSignBytes, err := proto.Marshal(signDocDirectAux) + require.NoError(t, err) + require.Equal(t, expectedSignBytes, signBytes) + + t.Log("verify GetSignBytes with false txBody data") + + signDocDirectAux.BodyBytes = []byte("dfafdasfds") + expectedSignBytes, err = proto.Marshal(signDocDirectAux) + require.NoError(t, err) + require.NotEqual(t, expectedSignBytes, signBytes) +} diff --git a/x/tx/signing/get_signers.go b/x/tx/signing/get_signers.go index 8ad36ce8cc..1eb27adafc 100644 --- a/x/tx/signing/get_signers.go +++ b/x/tx/signing/get_signers.go @@ -3,11 +3,12 @@ package signing import ( "fmt" - msgv1 "cosmossdk.io/api/cosmos/msg/v1" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protodesc" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" + + msgv1 "cosmossdk.io/api/cosmos/msg/v1" ) // GetSignersContext is a context for retrieving the list of signers from a