feat(x/tx): extract signers using cosmos.msg.v1.signer (#15205)

Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com>
This commit is contained in:
Aaron Craelius 2023-03-06 11:12:54 -05:00 committed by GitHub
parent 5f47935747
commit 1f40d9d47d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 6484 additions and 14 deletions

2
x/tx/Makefile Normal file
View File

@ -0,0 +1,2 @@
codegen:
@(cd internal/testpb; buf generate)

View File

@ -7,7 +7,7 @@ import "google/protobuf/descriptor.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
option go_package = "cosmossdk.io/x/tx/textual/internal/testpb";
option go_package = "cosmossdk.io/x/tx/internal/testpb";
// A is used for testing value renderers.
message A {

View File

@ -6725,11 +6725,11 @@ var file__1_proto_rawDesc = []byte{
0x02, 0x12, 0x14, 0x0a, 0x10, 0x42, 0x41, 0x4c, 0x4c, 0x4f, 0x54, 0x5f, 0x4f, 0x50, 0x54, 0x49,
0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x42, 0x41, 0x4c, 0x4c, 0x4f,
0x54, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x5f, 0x57, 0x49, 0x54, 0x48,
0x5f, 0x56, 0x45, 0x54, 0x4f, 0x10, 0x04, 0x42, 0x33, 0x42, 0x06, 0x31, 0x50, 0x72, 0x6f, 0x74,
0x6f, 0x50, 0x01, 0x5a, 0x27, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69,
0x6f, 0x2f, 0x74, 0x78, 0x2f, 0x74, 0x65, 0x78, 0x74, 0x75, 0x61, 0x6c, 0x2f, 0x69, 0x6e, 0x74,
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x5f, 0x56, 0x45, 0x54, 0x4f, 0x10, 0x04, 0x42, 0x35, 0x42, 0x06, 0x31, 0x50, 0x72, 0x6f, 0x74,
0x6f, 0x50, 0x01, 0x5a, 0x29, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69,
0x6f, 0x2f, 0x78, 0x2f, 0x74, 0x78, 0x2f, 0x74, 0x65, 0x78, 0x74, 0x75, 0x61, 0x6c, 0x2f, 0x69,
0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@ -0,0 +1,63 @@
syntax = "proto3";
import "cosmos/msg/v1/msg.proto";
option go_package = "cosmossdk.io/x/tx/internal/testpb";
message SimpleSigner {
option (cosmos.msg.v1.signer) = "signer";
string signer = 1;
}
message RepeatedSigner {
option (cosmos.msg.v1.signer) = "signer";
repeated string signer = 1;
}
message NestedSigner {
option (cosmos.msg.v1.signer) = "inner";
Inner inner = 1;
message Inner {
option (cosmos.msg.v1.signer) = "signer";
string signer = 1;
}
}
message RepeatedNestedSigner {
option (cosmos.msg.v1.signer) = "inner";
repeated Inner inner = 1;
message Inner {
option (cosmos.msg.v1.signer) = "signer";
string signer = 1;
}
}
message NestedRepeatedSigner {
option (cosmos.msg.v1.signer) = "inner";
Inner inner = 1;
message Inner {
option (cosmos.msg.v1.signer) = "signer";
repeated string signer = 1;
}
}
message RepeatedNestedRepeatedSigner {
option (cosmos.msg.v1.signer) = "inner";
repeated Inner inner = 1;
message Inner {
option (cosmos.msg.v1.signer) = "signer";
repeated string signer = 1;
}
}
message BadSigner {
option (cosmos.msg.v1.signer) = "signer";
bytes signer = 1;
}
message NoSignerOption {
bytes signer = 1;
}

File diff suppressed because it is too large Load Diff

177
x/tx/signing/get_signers.go Normal file
View File

@ -0,0 +1,177 @@
package signing
import (
"fmt"
msgv1 "cosmossdk.io/api/cosmos/msg/v1"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
// GetSignersContext is a context for retrieving the list of signers from a
// message where signers are specified by the cosmos.msg.v1.signer protobuf
// option.
type GetSignersContext struct {
protoFiles *protoregistry.Files
getSignersFuncs map[protoreflect.FullName]getSignersFunc
}
// GetSignersOptions are options for creating GetSignersContext.
type GetSignersOptions struct {
// ProtoFiles are the protobuf files to use for resolving message descriptors.
// If it is nil, the global protobuf registry will be used.
ProtoFiles *protoregistry.Files
}
// NewGetSignersContext creates a new GetSignersContext using the provided options.
func NewGetSignersContext(options GetSignersOptions) *GetSignersContext {
protoFiles := options.ProtoFiles
if protoFiles == nil {
protoFiles = protoregistry.GlobalFiles
}
return &GetSignersContext{
protoFiles: protoFiles,
getSignersFuncs: map[protoreflect.FullName]getSignersFunc{},
}
}
type getSignersFunc func(proto.Message) []string
func getSignersFieldNames(descriptor protoreflect.MessageDescriptor) ([]string, error) {
signersFields := proto.GetExtension(descriptor.Options(), msgv1.E_Signer).([]string)
if signersFields == nil || len(signersFields) == 0 {
return nil, fmt.Errorf("no cosmos.msg.v1.signer option found for message %s", descriptor.FullName())
}
return signersFields, nil
}
func (*GetSignersContext) makeGetSignersFunc(descriptor protoreflect.MessageDescriptor) (getSignersFunc, error) {
signersFields, err := getSignersFieldNames(descriptor)
if err != nil {
return nil, err
}
fieldGetters := make([]func(proto.Message, []string) []string, len(signersFields))
for i, fieldName := range signersFields {
field := descriptor.Fields().ByName(protoreflect.Name(fieldName))
if field == nil {
return nil, fmt.Errorf("field %s not found in message %s", fieldName, descriptor.FullName())
}
if field.IsMap() || field.HasOptionalKeyword() {
return nil, fmt.Errorf("cosmos.msg.v1.signer field %s in message %s must not be a map or optional", fieldName, descriptor.FullName())
}
switch field.Kind() {
case protoreflect.StringKind:
if field.IsList() {
fieldGetters[i] = func(msg proto.Message, arr []string) []string {
signers := msg.ProtoReflect().Get(field).List()
n := signers.Len()
for i := 0; i < n; i++ {
arr = append(arr, signers.Get(i).String())
}
return arr
}
} else {
fieldGetters[i] = func(msg proto.Message, arr []string) []string {
return append(arr, msg.ProtoReflect().Get(field).String())
}
}
case protoreflect.MessageKind:
isList := field.IsList()
nestedMessage := field.Message()
nestedSignersFields, err := getSignersFieldNames(nestedMessage)
if err != nil {
return nil, err
}
if len(nestedSignersFields) != 1 {
return nil, fmt.Errorf("nested cosmos.msg.v1.signer option in message %s must contain only one value", nestedMessage.FullName())
}
nestedFieldName := nestedSignersFields[0]
nestedField := nestedMessage.Fields().ByName(protoreflect.Name(nestedFieldName))
nestedIsList := nestedField.IsList()
if nestedField == nil {
return nil, fmt.Errorf("field %s not found in message %s", nestedFieldName, nestedMessage.FullName())
}
if nestedField.Kind() != protoreflect.StringKind || nestedField.IsMap() || nestedField.HasOptionalKeyword() {
return nil, fmt.Errorf("nested signer field %s in message %s must be a simple string", nestedFieldName, nestedMessage.FullName())
}
if isList {
if nestedIsList {
fieldGetters[i] = func(msg proto.Message, arr []string) []string {
msgs := msg.ProtoReflect().Get(field).List()
m := msgs.Len()
for i := 0; i < m; i++ {
signers := msgs.Get(i).Message().Get(nestedField).List()
n := signers.Len()
for j := 0; j < n; j++ {
arr = append(arr, signers.Get(j).String())
}
}
return arr
}
} else {
fieldGetters[i] = func(msg proto.Message, arr []string) []string {
msgs := msg.ProtoReflect().Get(field).List()
m := msgs.Len()
for i := 0; i < m; i++ {
arr = append(arr, msgs.Get(i).Message().Get(nestedField).String())
}
return arr
}
}
} else {
if nestedIsList {
fieldGetters[i] = func(msg proto.Message, arr []string) []string {
nestedMsg := msg.ProtoReflect().Get(field).Message()
signers := nestedMsg.Get(nestedField).List()
n := signers.Len()
for j := 0; j < n; j++ {
arr = append(arr, signers.Get(j).String())
}
return arr
}
} else {
fieldGetters[i] = func(msg proto.Message, arr []string) []string {
return append(arr, msg.ProtoReflect().Get(field).Message().Get(nestedField).String())
}
}
}
default:
return nil, fmt.Errorf("unexpected field type %s for field %s in message %s", field.Kind(), fieldName, descriptor.FullName())
}
}
return func(message proto.Message) []string {
var signers []string
for _, getter := range fieldGetters {
signers = getter(message, signers)
}
return signers
}, nil
}
// GetSigners returns the signers for a given message.
func (c *GetSignersContext) GetSigners(msg proto.Message) ([]string, error) {
messageDescriptor := msg.ProtoReflect().Descriptor()
f, ok := c.getSignersFuncs[messageDescriptor.FullName()]
if !ok {
var err error
f, err = c.makeGetSignersFunc(messageDescriptor)
if err != nil {
return nil, err
}
c.getSignersFuncs[messageDescriptor.FullName()] = f
}
return f(msg), nil
}

View File

@ -0,0 +1,113 @@
package signing
import (
"testing"
bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1"
groupv1 "cosmossdk.io/api/cosmos/group/v1"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"cosmossdk.io/x/tx/internal/testpb"
)
func TestGetSigners(t *testing.T) {
ctx := NewGetSignersContext(GetSignersOptions{})
tests := []struct {
name string
msg proto.Message
want []string
wantErr bool
}{
{
name: "MsgSend",
msg: &bankv1beta1.MsgSend{
FromAddress: "foo",
},
want: []string{"foo"},
},
{
name: "MsgMultiSend",
msg: &bankv1beta1.MsgMultiSend{
Inputs: []*bankv1beta1.Input{
{Address: "foo"},
{Address: "bar"},
},
},
want: []string{"foo", "bar"},
},
{
name: "MsgSubmitProposal",
msg: &groupv1.MsgSubmitProposal{
Proposers: []string{"foo", "bar"},
},
want: []string{"foo", "bar"},
},
{
name: "simple",
msg: &testpb.SimpleSigner{Signer: "foo"},
want: []string{"foo"},
},
{
name: "repeated",
msg: &testpb.RepeatedSigner{Signer: []string{"foo", "bar"}},
want: []string{"foo", "bar"},
},
{
name: "nested",
msg: &testpb.NestedSigner{Inner: &testpb.NestedSigner_Inner{Signer: "foo"}},
want: []string{"foo"},
},
{
name: "nested repeated",
msg: &testpb.NestedRepeatedSigner{Inner: &testpb.NestedRepeatedSigner_Inner{Signer: []string{"foo", "bar"}}},
want: []string{"foo", "bar"},
},
{
name: "repeated nested",
msg: &testpb.RepeatedNestedSigner{Inner: []*testpb.RepeatedNestedSigner_Inner{
{Signer: "foo"},
{Signer: "bar"},
}},
want: []string{"foo", "bar"},
},
{
name: "nested repeated",
msg: &testpb.NestedRepeatedSigner{Inner: &testpb.NestedRepeatedSigner_Inner{
Signer: []string{"foo", "bar"},
}},
want: []string{"foo", "bar"},
},
{
name: "repeated nested repeated",
msg: &testpb.RepeatedNestedRepeatedSigner{Inner: []*testpb.RepeatedNestedRepeatedSigner_Inner{
{Signer: []string{"foo", "bar"}},
{Signer: []string{"baz", "bam"}},
{Signer: []string{"blah"}},
}},
want: []string{"foo", "bar", "baz", "bam", "blah"},
},
{
name: "bad",
msg: &testpb.BadSigner{},
wantErr: true,
},
{
name: "no signer",
msg: &testpb.NoSignerOption{},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
signers, err := ctx.GetSigners(test.msg)
if test.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, test.want, signers)
})
}
}

View File

@ -13,8 +13,8 @@ import (
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/testing/protocmp"
"cosmossdk.io/x/tx/internal/testpb"
"cosmossdk.io/x/tx/textual"
"cosmossdk.io/x/tx/textual/internal/testpb"
)
type enumTest struct {

View File

@ -1,2 +0,0 @@
codegen:
@(buf generate)

View File

@ -13,10 +13,10 @@ import (
bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1"
"google.golang.org/protobuf/reflect/protoreflect"
"cosmossdk.io/x/tx/textual"
"cosmossdk.io/x/tx/textual/internal/testpb"
"google.golang.org/protobuf/testing/protocmp"
"cosmossdk.io/x/tx/internal/testpb"
"cosmossdk.io/x/tx/textual"
)
func EmptyCoinMetadataQuerier(ctx context.Context, denom string) (*bankv1beta1.Metadata, error) {

View File

@ -12,8 +12,8 @@ import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"cosmossdk.io/x/tx/internal/testpb"
"cosmossdk.io/x/tx/textual"
"cosmossdk.io/x/tx/textual/internal/testpb"
)
type repeatedJsonTest struct {

View File

@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/reflect/protoreflect"
"cosmossdk.io/x/tx/internal/testpb"
"cosmossdk.io/x/tx/textual"
"cosmossdk.io/x/tx/textual/internal/testpb"
)
func TestDispatcher(t *testing.T) {