package decode_test import ( "errors" "testing" "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/known/anypb" "cosmossdk.io/x/tx/decode" "cosmossdk.io/x/tx/internal/testpb" ) func errUnknownField(typ string, tagNum int, wireType protowire.Type) error { var wt string if wireType >= 0 && wireType < 6 { wt = decode.WireTypeToString(wireType) } return decode.ErrUnknownField.Wrapf("%s: {TagNum: %d, WireType:%q}", typ, tagNum, wt) } var ProtoResolver = protoregistry.GlobalFiles func TestRejectUnknownFieldsRepeated(t *testing.T) { tests := []struct { name string in proto.Message recv proto.Message wantErr error allowUnknownNonCriticals bool hasUnknownNonCriticals bool }{ { name: "Unknown field in midst of repeated values", in: &testpb.TestVersion2{ C: []*testpb.TestVersion2{ { C: []*testpb.TestVersion2{ { Sum: &testpb.TestVersion2_F{ F: &testpb.TestVersion2{ A: &testpb.TestVersion2{ B: &testpb.TestVersion2{ H: []*testpb.TestVersion1{ { X: 0x01, }, }, }, }, }, }, }, { Sum: &testpb.TestVersion2_F{ F: &testpb.TestVersion2{ A: &testpb.TestVersion2{ B: &testpb.TestVersion2{ H: []*testpb.TestVersion1{ { X: 0x02, }, }, }, }, }, }, }, { Sum: &testpb.TestVersion2_F{ F: &testpb.TestVersion2{ NewField_: 411, }, }, }, }, }, }, }, recv: new(testpb.TestVersion1), wantErr: errUnknownField( "testpb.TestVersion1", 25, 0), }, { name: "Unknown field in midst of repeated values, allowUnknownNonCriticals set", allowUnknownNonCriticals: true, in: &testpb.TestVersion2{ C: []*testpb.TestVersion2{ { C: []*testpb.TestVersion2{ { Sum: &testpb.TestVersion2_F{ F: &testpb.TestVersion2{ A: &testpb.TestVersion2{ B: &testpb.TestVersion2{ H: []*testpb.TestVersion1{ { X: 0x01, }, }, }, }, }, }, }, { Sum: &testpb.TestVersion2_F{ F: &testpb.TestVersion2{ A: &testpb.TestVersion2{ B: &testpb.TestVersion2{ H: []*testpb.TestVersion1{ { X: 0x02, }, }, }, }, }, }, }, { Sum: &testpb.TestVersion2_F{ F: &testpb.TestVersion2{ NewField_: 411, }, }, }, }, }, }, }, recv: new(testpb.TestVersion1), wantErr: errUnknownField( "testpb.TestVersion1", 25, 0), }, { name: "Unknown field in midst of repeated values, non-critical field to be rejected", in: &testpb.TestVersion3{ C: []*testpb.TestVersion3{ { C: []*testpb.TestVersion3{ { Sum: &testpb.TestVersion3_F{ F: &testpb.TestVersion3{ A: &testpb.TestVersion3{ B: &testpb.TestVersion3{ X: 0x01, }, }, }, }, }, { Sum: &testpb.TestVersion3_F{ F: &testpb.TestVersion3{ A: &testpb.TestVersion3{ B: &testpb.TestVersion3{ X: 0x02, }, }, }, }, }, { Sum: &testpb.TestVersion3_F{ F: &testpb.TestVersion3{ NonCriticalField: "non-critical", }, }, }, }, }, }, }, recv: new(testpb.TestVersion1), wantErr: errUnknownField( "testpb.TestVersion1", 1031, 2), hasUnknownNonCriticals: true, }, { name: "Unknown field in midst of repeated values, non-critical field ignored", allowUnknownNonCriticals: true, in: &testpb.TestVersion3{ C: []*testpb.TestVersion3{ { C: []*testpb.TestVersion3{ { Sum: &testpb.TestVersion3_F{ F: &testpb.TestVersion3{ A: &testpb.TestVersion3{ B: &testpb.TestVersion3{ X: 0x01, }, }, }, }, }, { Sum: &testpb.TestVersion3_F{ F: &testpb.TestVersion3{ A: &testpb.TestVersion3{ B: &testpb.TestVersion3{ X: 0x02, }, }, }, }, }, { Sum: &testpb.TestVersion3_F{ F: &testpb.TestVersion3{ NonCriticalField: "non-critical", }, }, }, }, }, }, }, recv: new(testpb.TestVersion1), wantErr: nil, hasUnknownNonCriticals: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { protoBlob, err := proto.Marshal(tt.in) if err != nil { t.Fatal(err) } desc := tt.recv.ProtoReflect().Descriptor() hasUnknownNonCriticals, gotErr := decode.RejectUnknownFields( protoBlob, desc, tt.allowUnknownNonCriticals, ProtoResolver) if tt.wantErr != nil { require.EqualError(t, gotErr, tt.wantErr.Error()) } else { require.NoError(t, gotErr) } require.Equal(t, tt.hasUnknownNonCriticals, hasUnknownNonCriticals) }) } } func TestRejectUnknownFields_allowUnknownNonCriticals(t *testing.T) { tests := []struct { name string in proto.Message allowUnknownNonCriticals bool wantErr error }{ { name: "Field that's in the reserved range, should fail by default", in: &testpb.Customer2{ Id: 289, Reserved: 99, }, wantErr: errUnknownField( "testpb.Customer1", 1047, 0), }, { name: "Field that's in the reserved range, toggle allowUnknownNonCriticals", allowUnknownNonCriticals: true, in: &testpb.Customer2{ Id: 289, Reserved: 99, }, wantErr: nil, }, { name: "Unknown fields that are critical, but with allowUnknownNonCriticals set", allowUnknownNonCriticals: true, in: &testpb.Customer2{ Id: 289, City: testpb.Customer2_PaloAlto, }, wantErr: errUnknownField( "testpb.Customer1", 6, 0), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { blob, err := proto.Marshal(tt.in) if err != nil { t.Fatalf("Failed to marshal input: %v", err) } c1 := new(testpb.Customer1).ProtoReflect().Descriptor() _, gotErr := decode.RejectUnknownFields(blob, c1, tt.allowUnknownNonCriticals, ProtoResolver) if tt.wantErr != nil { require.EqualError(t, gotErr, tt.wantErr.Error()) } else { require.NoError(t, gotErr) } }) } } func TestRejectUnknownFieldsNested(t *testing.T) { tests := []struct { name string in proto.Message recv proto.Message wantErr error }{ { name: "TestVersion3 from TestVersionFD1", in: &testpb.TestVersion2{ X: 5, Sum: &testpb.TestVersion2_E{ E: 100, }, H: []*testpb.TestVersion1{ {X: 999}, {X: -55}, { X: 102, Sum: &testpb.TestVersion1_F{ F: &testpb.TestVersion1{ X: 4, }, }, }, }, K: &testpb.Customer1{ Id: 45, Name: "customer1", SubscriptionFee: 99, }, }, recv: new(testpb.TestVersionFD1), wantErr: errUnknownField( "testpb.TestVersionFD1", 12, 2), }, { name: "Alternating oneofs", in: &testpb.TestVersion3{ Sum: &testpb.TestVersion3_E{ E: 99, }, }, recv: new(testpb.TestVersion3LoneOneOfValue), wantErr: nil, }, { name: "Alternating oneofs mismatched field", in: &testpb.TestVersion3{ Sum: &testpb.TestVersion3_F{ F: &testpb.TestVersion3{ X: 99, }, }, }, recv: new(testpb.TestVersion3LoneOneOfValue), wantErr: errUnknownField( "testpb.TestVersion3LoneOneOfValue", 7, 2), }, { name: "Discrepancy in a deeply nested one of field", in: &testpb.TestVersion3{ Sum: &testpb.TestVersion3_F{ F: &testpb.TestVersion3{ Sum: &testpb.TestVersion3_F{ F: &testpb.TestVersion3{ X: 19, Sum: &testpb.TestVersion3_E{ E: 99, }, }, }, }, }, }, recv: new(testpb.TestVersion3LoneNesting), wantErr: errUnknownField( "testpb.TestVersion3LoneNesting", 6, 0), }, { name: "unknown field types.Any in G", in: &testpb.TestVersion3{ G: &anypb.Any{ TypeUrl: "/testpb.TestVersion1", Value: mustMarshal(&testpb.TestVersion2{ Sum: &testpb.TestVersion2_F{ F: &testpb.TestVersion2{ NewField_: 999, }, }, }), }, }, recv: new(testpb.TestVersion3), wantErr: errUnknownField( "testpb.TestVersion1", 25, 0), }, { name: "types.Any with extra fields", in: &testpb.TestVersionFD1WithExtraAny{ G: &testpb.AnyWithExtra{ A: &anypb.Any{ TypeUrl: "/testpb.TestVersion1", Value: mustMarshal(&testpb.TestVersion2{ Sum: &testpb.TestVersion2_F{ F: &testpb.TestVersion2{ NewField_: 999, }, }, }), }, B: 3, C: 2, }, }, recv: new(testpb.TestVersion3), wantErr: errUnknownField( "google.protobuf.Any", 3, 0), }, { name: "mismatched types.Any in G", in: &testpb.TestVersion1{ G: &anypb.Any{ TypeUrl: "/testpb.TestVersion4LoneNesting", Value: mustMarshal(&testpb.TestVersion3LoneNesting_Inner1{ Inner: &testpb.TestVersion3LoneNesting_Inner1_InnerInner{ Id: "ID", City: "Gotham", }, }), }, }, recv: new(testpb.TestVersion1), // behavior change from previous implementation: we allow mismatched wire -> proto types, // but this will still error on ConsumeFieldValue wantErr: errors.New("cannot parse reserved wire type"), }, { name: "From nested proto message, message index 0", in: &testpb.TestVersion3LoneNesting{ Inner1: &testpb.TestVersion3LoneNesting_Inner1{ Id: 10, Name: "foo", Inner: &testpb.TestVersion3LoneNesting_Inner1_InnerInner{ Id: "ID", City: "Palo Alto", }, }, }, recv: new(testpb.TestVersion4LoneNesting), wantErr: nil, }, { name: "From nested proto message, message index 1", in: &testpb.TestVersion3LoneNesting{ Inner2: &testpb.TestVersion3LoneNesting_Inner2{ Id: "ID", Country: "Maldives", Inner: &testpb.TestVersion3LoneNesting_Inner2_InnerInner{ Id: "ID", City: "Unknown", }, }, }, recv: new(testpb.TestVersion4LoneNesting), wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { protoBlob, err := proto.Marshal(tt.in) if err != nil { t.Fatal(err) } desc := tt.recv.ProtoReflect().Descriptor() gotErr := decode.RejectUnknownFieldsStrict(protoBlob, desc, ProtoResolver) if tt.wantErr != nil { require.ErrorContains(t, gotErr, tt.wantErr.Error()) } else { require.NoError(t, gotErr) } }) } } func TestRejectUnknownFieldsFlat(t *testing.T) { tests := []struct { name string in proto.Message wantErr error }{ { name: "Oneof with same field number, shouldn't complain", in: &testpb.Customer3{ Id: 68, Name: "ACME3", Payment: &testpb.Customer3_CreditCardNo{ CreditCardNo: "123-XXXX-XXX881", }, }, wantErr: nil, }, { name: "Oneof with different field number, should fail", in: &testpb.Customer3{ Id: 68, Name: "ACME3", Payment: &testpb.Customer3_ChequeNo{ ChequeNo: "123XXXXXXX881", }, }, wantErr: errUnknownField( "testpb.Customer1", 8, 2), }, { name: "Any in a field, the extra field will be serialized so should fail", in: &testpb.Customer2{ Miscellaneous: &anypb.Any{}, }, wantErr: errUnknownField( "testpb.Customer1", 10, 2), }, { name: "With a nested struct as a field", in: &testpb.Customer3{ Id: 289, Original: &testpb.Customer1{ Id: 991, }, }, wantErr: errUnknownField( "testpb.Customer1", 9, 2), }, { name: "An extra field that's non-existent in Customer1", in: &testpb.Customer2{ Id: 289, Name: "Customer1", Industry: 5299, Fewer: 199.9, }, wantErr: errUnknownField("testpb.Customer1", 4, 5), }, { name: "Using a field that's in the reserved range, should fail by default", in: &testpb.Customer2{ Id: 289, Reserved: 99, }, wantErr: errUnknownField( "testpb.Customer1", 1047, 0), }, { name: "Only fields matching", in: &testpb.Customer2{ Id: 289, Name: "CustomerCustomerCustomerCustomerCustomer11111Customer1", }, // behavior change from previous implementation: we allow mismatched wire -> proto types. // wantErr: errMismatchedField("testpb.Customer1", 4, 5), }, { name: "Extra field that's non-existent in Customer1, along with Reserved set", in: &testpb.Customer2{ Id: 289, Name: "Customer1", Industry: 5299, Fewer: 199.9, Reserved: 819, }, wantErr: errUnknownField("testpb.Customer1", 4, 5), }, { name: "Using enumerated field", in: &testpb.Customer2{ Id: 289, Name: "Customer1", Industry: 5299, City: testpb.Customer2_PaloAlto, }, wantErr: errUnknownField("testpb.Customer1", 6, 0), }, { name: "multiple extraneous fields", in: &testpb.Customer2{ Id: 289, Name: "Customer1", Industry: 5299, City: testpb.Customer2_PaloAlto, Fewer: 45, }, wantErr: errUnknownField("testpb.Customer1", 4, 5), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { blob, err := proto.Marshal(tt.in) if err != nil { t.Fatalf("Failed to marshal input: %v", err) } c1 := new(testpb.Customer1) c1Desc := c1.ProtoReflect().Descriptor() gotErr := decode.RejectUnknownFieldsStrict(blob, c1Desc, ProtoResolver) if tt.wantErr != nil { require.EqualError(t, gotErr, tt.wantErr.Error()) } else { require.NoError(t, gotErr) } }) } } // Issue https://github.com/cosmos/cosmos-sdk/issues/7222, we need to ensure that repeated // uint64 are recognized as packed. func TestPackedEncoding(t *testing.T) { data := &testpb.TestRepeatedUints{Nums: []uint64{12, 13}} marshaled, err := proto.Marshal(data) require.NoError(t, err) unmarshalled := data.ProtoReflect().Descriptor() _, err = decode.RejectUnknownFields(marshaled, unmarshalled, false, ProtoResolver) require.NoError(t, err) } func mustMarshal(msg proto.Message) []byte { blob, err := proto.Marshal(msg) if err != nil { panic(err) } return blob }