From 9332942f3723b3c3bf0d055c34fb84e7edc952ff Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 31 Mar 2023 10:36:06 -0400 Subject: [PATCH] feat(codec)!: add get signers to codec + merged proto registry to InterfaceRegistry (#15600) Co-authored-by: Matt Kocubinski --- CHANGELOG.md | 6 ++++ client/grpc_query.go | 37 +--------------------- client/internal_client_test.go | 31 ------------------ codec/amino_codec.go | 31 ++++++++++++++++-- codec/codec.go | 21 +++++++++++++ codec/proto_codec.go | 52 +++++++++++++++++++++++++++++-- codec/proto_codec_test.go | 36 +++++++++++++++++++++ codec/types/interface_registry.go | 28 +++++++++++++++++ runtime/module.go | 35 +++++++++++---------- 9 files changed, 187 insertions(+), 90 deletions(-) delete mode 100644 client/internal_client_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 16897a0b6f36..1e0cb9893e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (crypto) [#15070](https://github.com/cosmos/cosmos-sdk/pull/15070) `GenerateFromPassword` and `Cost` from `bcrypt.go` now take a `uint32` instead of a `int` type. * (x/capability) [#15344](https://github.com/cosmos/cosmos-sdk/pull/15344) Capability module was removed and is now housed in [IBC-GO](https://github.com/cosmos/ibc-go). * [#15299](https://github.com/cosmos/cosmos-sdk/pull/15299) remove `StdTx` transaction and signing APIs. No SDK version has actually supported `StdTx` since before Stargate. +* [#15600](https://github.com/cosmos/cosmos-sdk/pull/15600) add support for getting signers to `codec.Codec` and protoregistry support to `InterfaceRegistry`: + * `Codec` is now a private interface and has the methods `InterfaceRegistry`, `GetMsgAnySigners`, `GetMsgV1Signers`, and `GetMsgV2Signers` which will fail when using `AminoCodec`. + All implementations of `Codec` by other users must now embed an official implementation from the `codec` package. + * `InterfaceRegistry` is now a private interface and implements `protodesc.Resolver` plus the `RangeFiles` method + All implementations of `InterfaceRegistry` by other users must now embed the official implementation. + * `AminoCodec` is marked as deprecated. ### Client Breaking Changes diff --git a/client/grpc_query.go b/client/grpc_query.go index 534211518367..b79e2a1f6845 100644 --- a/client/grpc_query.go +++ b/client/grpc_query.go @@ -7,7 +7,6 @@ import ( "reflect" "strconv" - proto "github.com/cosmos/gogoproto/proto" "google.golang.org/grpc/encoding" "github.com/cosmos/cosmos-sdk/codec" @@ -29,7 +28,7 @@ var _ gogogrpc.ClientConn = Context{} // fallBackCodec is used by Context in case Codec is not set. // it can process every gRPC type, except the ones which contain // interfaces in their types. -var fallBackCodec = codec.NewProtoCodec(failingInterfaceRegistry{}) +var fallBackCodec = codec.NewProtoCodec(types.NewInterfaceRegistry()) // Invoke implements the grpc ClientConn.Invoke method func (ctx Context) Invoke(grpcCtx gocontext.Context, method string, req, reply interface{}, opts ...grpc.CallOption) (err error) { @@ -144,40 +143,6 @@ func (ctx Context) gRPCCodec() encoding.Codec { return pc.GRPCCodec() } -var _ types.InterfaceRegistry = failingInterfaceRegistry{} - -// failingInterfaceRegistry is used by the fallback codec -// in case Context's Codec is not set. -type failingInterfaceRegistry struct{} - // errCodecNotSet is return by failingInterfaceRegistry in case there are attempt to decode // or encode a type which contains an interface field. var errCodecNotSet = errors.New("client: cannot encode or decode type which requires the application specific codec") - -func (f failingInterfaceRegistry) UnpackAny(any *types.Any, iface interface{}) error { - return errCodecNotSet -} - -func (f failingInterfaceRegistry) Resolve(typeURL string) (proto.Message, error) { - return nil, errCodecNotSet -} - -func (f failingInterfaceRegistry) RegisterInterface(protoName string, iface interface{}, impls ...proto.Message) { - panic("cannot be called") -} - -func (f failingInterfaceRegistry) RegisterImplementations(iface interface{}, impls ...proto.Message) { - panic("cannot be called") -} - -func (f failingInterfaceRegistry) ListAllInterfaces() []string { - panic("cannot be called") -} - -func (f failingInterfaceRegistry) ListImplementations(ifaceTypeURL string) []string { - panic("cannot be called") -} - -func (f failingInterfaceRegistry) EnsureRegistered(iface interface{}) error { - panic("cannot be called") -} diff --git a/client/internal_client_test.go b/client/internal_client_test.go deleted file mode 100644 index 6d6cacb87f82..000000000000 --- a/client/internal_client_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package client - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestFailingInterfaceRegistry(t *testing.T) { - reg := failingInterfaceRegistry{} - - require.Error(t, reg.UnpackAny(nil, nil)) - _, err := reg.Resolve("") - require.Error(t, err) - - require.Panics(t, func() { - reg.RegisterInterface("", nil) - }) - require.Panics(t, func() { - reg.RegisterImplementations(nil, nil) - }) - require.Panics(t, func() { - reg.ListAllInterfaces() - }) - require.Panics(t, func() { - reg.ListImplementations("") - }) - require.Panics(t, func() { - reg.EnsureRegistered(nil) - }) -} diff --git a/codec/amino_codec.go b/codec/amino_codec.go index d77acbc7e8e0..2b1e02864513 100644 --- a/codec/amino_codec.go +++ b/codec/amino_codec.go @@ -1,18 +1,25 @@ package codec import ( + "fmt" + "github.com/cosmos/gogoproto/proto" + protov2 "google.golang.org/protobuf/proto" + + "github.com/cosmos/cosmos-sdk/codec/types" ) -// AminoCodec defines a codec that utilizes Codec for both binary and JSON -// encoding. +// Deprecated: AminoCodec defines a codec that utilizes Codec for both binary and JSON +// encoding. Any usage of amino should be done using the LegacyAmino type directly. +// Usage of amino with the Codec type is not well-supported and may be removed in the future. type AminoCodec struct { *LegacyAmino } var _ Codec = &AminoCodec{} -// NewAminoCodec returns a reference to a new AminoCodec +// Deprecated: NewAminoCodec returns a reference to a new AminoCodec. +// Use NewLegacyAmino instead. func NewAminoCodec(codec *LegacyAmino) *AminoCodec { return &AminoCodec{LegacyAmino: codec} } @@ -124,3 +131,21 @@ func (ac *AminoCodec) MarshalInterfaceJSON(i proto.Message) ([]byte, error) { func (ac *AminoCodec) UnmarshalInterfaceJSON(bz []byte, ptr interface{}) error { return ac.LegacyAmino.UnmarshalJSON(bz, ptr) } + +func (ac *AminoCodec) GetMsgAnySigners(*types.Any) ([]string, protov2.Message, error) { + return nil, nil, fmt.Errorf("amino codec does not support getting msg signers") +} + +func (ac *AminoCodec) GetMsgV1Signers(proto.Message) ([]string, protov2.Message, error) { + return nil, nil, fmt.Errorf("amino codec does not support getting msg signers") +} + +func (ac *AminoCodec) GetMsgV2Signers(protov2.Message) ([]string, error) { + return nil, fmt.Errorf("amino codec does not support getting msg signers") +} + +func (ac *AminoCodec) InterfaceRegistry() types.InterfaceRegistry { + panic("amino codec does not support interface registry") +} + +func (ac *AminoCodec) mustEmbedCodec() {} diff --git a/codec/codec.go b/codec/codec.go index 5fe0e98e89f3..7bdaffa69bf5 100644 --- a/codec/codec.go +++ b/codec/codec.go @@ -3,6 +3,7 @@ package codec import ( "github.com/cosmos/gogoproto/proto" "google.golang.org/grpc/encoding" + protov2 "google.golang.org/protobuf/proto" "github.com/cosmos/cosmos-sdk/codec/types" ) @@ -18,6 +19,26 @@ type ( Codec interface { BinaryCodec JSONCodec + + // InterfaceRegistry returns the interface registry. + InterfaceRegistry() types.InterfaceRegistry + + // GetMsgAnySigners returns the signers of the given message encoded in a protobuf Any + // as well as the decoded google.golang.org/protobuf/proto.Message that was used to + // extract the signers so that this can be used in other contexts. + GetMsgAnySigners(msg *types.Any) ([]string, protov2.Message, error) + + // GetMsgV2Signers returns the signers of the given message. + GetMsgV2Signers(msg protov2.Message) ([]string, error) + + // GetMsgV1Signers returns the signers of the given message plus the + // decoded google.golang.org/protobuf/proto.Message that was used to extract the + // signers so that this can be used in other contexts. + GetMsgV1Signers(msg proto.Message) ([]string, protov2.Message, error) + + // mustEmbedCodec requires that all implementations of Codec embed an official implementation from the codec + // package. This allows new methods to be added to the Codec interface without breaking backwards compatibility. + mustEmbedCodec() } BinaryCodec interface { diff --git a/codec/proto_codec.go b/codec/proto_codec.go index 6d4fec199bdd..212faf1d838b 100644 --- a/codec/proto_codec.go +++ b/codec/proto_codec.go @@ -6,11 +6,14 @@ import ( "fmt" "strings" + "github.com/cosmos/cosmos-proto/anyutil" + "github.com/cosmos/gogoproto/jsonpb" + gogoproto "github.com/cosmos/gogoproto/proto" "google.golang.org/grpc/encoding" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" - "github.com/cosmos/gogoproto/jsonpb" - gogoproto "github.com/cosmos/gogoproto/proto" + "cosmossdk.io/x/tx/signing" "github.com/cosmos/cosmos-sdk/codec/types" ) @@ -26,6 +29,7 @@ type ProtoCodecMarshaler interface { // encoding. type ProtoCodec struct { interfaceRegistry types.InterfaceRegistry + getSignersCtx *signing.GetSignersContext } var ( @@ -35,7 +39,17 @@ var ( // NewProtoCodec returns a reference to a new ProtoCodec func NewProtoCodec(interfaceRegistry types.InterfaceRegistry) *ProtoCodec { - return &ProtoCodec{interfaceRegistry: interfaceRegistry} + getSignersCtx, err := signing.NewGetSignersContext( + signing.GetSignersOptions{ + ProtoFiles: interfaceRegistry, + }) + if err != nil { + panic(err) + } + return &ProtoCodec{ + interfaceRegistry: interfaceRegistry, + getSignersCtx: getSignersCtx, + } } // Marshal implements BinaryMarshaler.Marshal method. @@ -265,11 +279,43 @@ func (pc *ProtoCodec) InterfaceRegistry() types.InterfaceRegistry { return pc.interfaceRegistry } +func (pc ProtoCodec) GetMsgAnySigners(msg *types.Any) ([]string, proto.Message, error) { + msgv2, err := anyutil.Unpack(&anypb.Any{ + TypeUrl: msg.TypeUrl, + Value: msg.Value, + }, pc.interfaceRegistry, nil) + if err != nil { + return nil, nil, err + } + + signers, err := pc.getSignersCtx.GetSigners(msgv2) + return signers, msgv2, err +} + +func (pc *ProtoCodec) GetMsgV2Signers(msg proto.Message) ([]string, error) { + return pc.getSignersCtx.GetSigners(msg) +} + +func (pc *ProtoCodec) GetMsgV1Signers(msg gogoproto.Message) ([]string, proto.Message, error) { + if msgV2, ok := msg.(proto.Message); ok { + signers, err := pc.getSignersCtx.GetSigners(msgV2) + return signers, msgV2, err + } else { + a, err := types.NewAnyWithValue(msg) + if err != nil { + return nil, nil, err + } + return pc.GetMsgAnySigners(a) + } +} + // GRPCCodec returns the gRPC Codec for this specific ProtoCodec func (pc *ProtoCodec) GRPCCodec() encoding.Codec { return &grpcProtoCodec{cdc: pc} } +func (pc *ProtoCodec) mustEmbedCodec() {} + var errUnknownProtoType = errors.New("codec: unknown proto type") // sentinel error // grpcProtoCodec is the implementation of the gRPC proto codec. diff --git a/codec/proto_codec_test.go b/codec/proto_codec_test.go index 9838be6b6cf2..f0591dad4ef1 100644 --- a/codec/proto_codec_test.go +++ b/codec/proto_codec_test.go @@ -5,15 +5,19 @@ import ( "reflect" "testing" + bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" + basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/encoding" "google.golang.org/grpc/status" + protov2 "google.golang.org/protobuf/proto" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) @@ -168,3 +172,35 @@ func BenchmarkProtoCodecMarshalLengthPrefixed(b *testing.B) { b.SetBytes(int64(len(blob))) } } + +func TestGetSigners(t *testing.T) { + interfaceRegistry := types.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + testAddr := sdk.AccAddress([]byte("test")) + testAddrStr := testAddr.String() + testAddr2 := sdk.AccAddress([]byte("test2")) + testAddrStr2 := testAddr2.String() + + msgSendV1 := banktypes.NewMsgSend(testAddr, testAddr2, sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1)))) + msgSendV2 := &bankv1beta1.MsgSend{ + FromAddress: testAddrStr, + ToAddress: testAddrStr2, + Amount: []*basev1beta1.Coin{{Denom: "foo", Amount: "1"}}, + } + + signers, msgSendV2Copy, err := cdc.GetMsgV1Signers(msgSendV1) + require.NoError(t, err) + require.Equal(t, []string{testAddrStr}, signers) + require.True(t, protov2.Equal(msgSendV2, msgSendV2Copy)) + + signers, err = cdc.GetMsgV2Signers(msgSendV2) + require.NoError(t, err) + require.Equal(t, []string{testAddrStr}, signers) + + msgSendAny, err := types.NewAnyWithValue(msgSendV1) + require.NoError(t, err) + signers, msgSendV2Copy, err = cdc.GetMsgAnySigners(msgSendAny) + require.NoError(t, err) + require.Equal(t, []string{testAddrStr}, signers) + require.True(t, protov2.Equal(msgSendV2, msgSendV2Copy)) +} diff --git a/codec/types/interface_registry.go b/codec/types/interface_registry.go index b911cb6bf6e9..57a098e0cbe4 100644 --- a/codec/types/interface_registry.go +++ b/codec/types/interface_registry.go @@ -6,6 +6,9 @@ import ( "github.com/cosmos/gogoproto/jsonpb" "github.com/cosmos/gogoproto/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" ) // AnyUnpacker is an interface which allows safely unpacking types packed @@ -54,6 +57,18 @@ type InterfaceRegistry interface { // EnsureRegistered ensures there is a registered interface for the given concrete type. EnsureRegistered(iface interface{}) error + + protodesc.Resolver + + // RangeFiles iterates over all registered files and calls f on each one. This + // implements the part of protoregistry.Files that is needed for reflecting over + // the entire FileDescriptorSet. + RangeFiles(f func(protoreflect.FileDescriptor) bool) + + // mustEmbedInterfaceRegistry requires that all implementations of InterfaceRegistry embed an official implementation + // from this package. This allows new methods to be added to the InterfaceRegistry interface without breaking + // backwards compatibility. + mustEmbedInterfaceRegistry() } // UnpackInterfacesMessage is meant to extend protobuf types (which implement @@ -81,6 +96,7 @@ type UnpackInterfacesMessage interface { } type interfaceRegistry struct { + *protoregistry.Files interfaceNames map[string]reflect.Type interfaceImpls map[reflect.Type]interfaceMap implInterfaces map[reflect.Type]reflect.Type @@ -91,11 +107,21 @@ type interfaceMap = map[string]reflect.Type // NewInterfaceRegistry returns a new InterfaceRegistry func NewInterfaceRegistry() InterfaceRegistry { + protoFiles, err := proto.MergedRegistry() + if err != nil { + panic(err) + } + return NewInterfaceRegistryWithProtoFiles(protoFiles) +} + +// NewInterfaceRegistryWithProtoFiles returns a new InterfaceRegistry with the specified *protoregistry.Files instance. +func NewInterfaceRegistryWithProtoFiles(files *protoregistry.Files) InterfaceRegistry { return &interfaceRegistry{ interfaceNames: map[string]reflect.Type{}, interfaceImpls: map[reflect.Type]interfaceMap{}, implInterfaces: map[reflect.Type]reflect.Type{}, typeURLMap: map[string]reflect.Type{}, + Files: files, } } @@ -288,6 +314,8 @@ func (registry *interfaceRegistry) Resolve(typeURL string) (proto.Message, error return msg, nil } +func (registry *interfaceRegistry) mustEmbedInterfaceRegistry() {} + // UnpackInterfaces is a convenience function that calls UnpackInterfaces // on x if x implements UnpackInterfacesMessage func UnpackInterfaces(x interface{}, unpacker AnyUnpacker) error { diff --git a/runtime/module.go b/runtime/module.go index 19751195fe7a..82ee40470fba 100644 --- a/runtime/module.go +++ b/runtime/module.go @@ -5,6 +5,7 @@ import ( "os" "cosmossdk.io/core/store" + "github.com/cosmos/gogoproto/proto" "google.golang.org/protobuf/reflect/protodesc" "google.golang.org/protobuf/reflect/protoregistry" @@ -24,7 +25,6 @@ import ( "github.com/cosmos/cosmos-sdk/std" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/types/msgservice" - "github.com/cosmos/gogoproto/proto" ) type appModule struct { @@ -80,8 +80,23 @@ func ProvideApp() ( appmodule.AppModule, protodesc.Resolver, protoregistry.MessageTypeResolver, + error, ) { - interfaceRegistry := codectypes.NewInterfaceRegistry() + protoFiles, err := proto.MergedRegistry() + if err != nil { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err + } + protoTypes := protoregistry.GlobalTypes + + // At startup, check that all proto annotations are correct. + err = msgservice.ValidateProtoAnnotations(protoFiles) + if err != nil { + // Once we switch to using protoreflect-based antehandlers, we might + // want to panic here instead of logging a warning. + _, _ = fmt.Fprintln(os.Stderr, err.Error()) + } + + interfaceRegistry := codectypes.NewInterfaceRegistryWithProtoFiles(protoFiles) amino := codec.NewLegacyAmino() std.RegisterInterfaces(interfaceRegistry) @@ -99,21 +114,7 @@ func ProvideApp() ( } appBuilder := &AppBuilder{app} - protoFiles, err := proto.MergedRegistry() - if err != nil { - panic(err) - } - protoTypes := protoregistry.GlobalTypes - - // At startup, check that all proto annotations are correct. - err = msgservice.ValidateProtoAnnotations(protoFiles) - if err != nil { - // Once we switch to using protoreflect-based antehandlers, we might - // want to panic here instead of logging a warning. - fmt.Fprintln(os.Stderr, err.Error()) - } - - return interfaceRegistry, cdc, amino, appBuilder, cdc, msgServiceRouter, appModule{app}, protoFiles, protoTypes + return interfaceRegistry, cdc, amino, appBuilder, cdc, msgServiceRouter, appModule{app}, protoFiles, protoTypes, nil } type AppInputs struct {