rlp: add support for optional struct fields (#22832) (#3977)

This adds support for a new struct tag "optional". Using this tag, structs used
for RLP encoding/decoding can be extended in a backwards-compatible way,
by adding new fields at the end.

see geth commit 700df1442d

Co-authored-by: Felix Lange <fjl@twurst.com>
This commit is contained in:
battlmonstr 2022-04-26 13:24:23 +02:00 committed by GitHub
parent 61515ed21e
commit 04f3e9004c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 335 additions and 50 deletions

View File

@ -444,9 +444,16 @@ func makeStructDecoder(typ reflect.Type) (decoder, error) {
if _, err := s.List(); err != nil {
return wrapStreamError(err, typ)
}
for _, f := range fields {
for i, f := range fields {
err := f.info.decoder(s, val.Field(f.index))
if err == EOL {
if f.optional {
// The field is optional, so reaching the end of the list before
// reaching the last field is acceptable. All remaining undecoded
// fields are zeroed.
zeroFields(val, fields[i:])
break
}
return &decodeError{msg: "too few elements", typ: typ}
} else if err != nil {
return addErrorContext(err, "."+typ.Field(f.index).Name)
@ -457,6 +464,13 @@ func makeStructDecoder(typ reflect.Type) (decoder, error) {
return dec, nil
}
func zeroFields(structval reflect.Value, fields []field) {
for _, f := range fields {
fv := structval.Field(f.index)
fv.Set(reflect.Zero(fv.Type()))
}
}
// makePtrDecoder creates a decoder that decodes into the pointer's element type.
func makePtrDecoder(typ reflect.Type, tag tags) (decoder, error) {
etype := typ.Elem()

View File

@ -374,6 +374,39 @@ type intField struct {
X int
}
type optionalFields struct {
A uint
B uint `rlp:"optional"`
C uint `rlp:"optional"`
}
type optionalAndTailField struct {
A uint
B uint `rlp:"optional"`
Tail []uint `rlp:"tail"`
}
type optionalBigIntField struct {
A uint
B *big.Int `rlp:"optional"`
}
type optionalPtrField struct {
A uint
B *[3]byte `rlp:"optional"`
}
type optionalPtrFieldNil struct {
A uint
B *[3]byte `rlp:"optional,nil"`
}
type ignoredField struct {
A uint
B uint `rlp:"-"`
C uint
}
var (
veryBigInt = uint256.NewInt(0).Add(
uint256.NewInt(0).Lsh(uint256.NewInt(0xFFFFFFFFFFFFFF), 16),
@ -383,12 +416,6 @@ var (
veryVeryBigInt = new(big.Int).Exp(veryBigInt.ToBig(), big.NewInt(8), nil)
)
type hasIgnoredField struct {
A uint
B uint `rlp:"-"`
C uint
}
var decodeTests = []decodeTest{
// booleans
{input: "01", ptr: new(bool), value: true},
@ -578,8 +605,8 @@ var decodeTests = []decodeTest{
// struct tag "-"
{
input: "C20102",
ptr: new(hasIgnoredField),
value: hasIgnoredField{A: 1, C: 2},
ptr: new(ignoredField),
value: ignoredField{A: 1, C: 2},
},
// struct tag "nilList"
@ -619,6 +646,110 @@ var decodeTests = []decodeTest{
value: nilStringSlice{X: &[]uint{3}},
},
// struct tag "optional"
{
input: "C101",
ptr: new(optionalFields),
value: optionalFields{1, 0, 0},
},
{
input: "C20102",
ptr: new(optionalFields),
value: optionalFields{1, 2, 0},
},
{
input: "C3010203",
ptr: new(optionalFields),
value: optionalFields{1, 2, 3},
},
{
input: "C401020304",
ptr: new(optionalFields),
error: "rlp: input list has too many elements for rlp.optionalFields",
},
{
input: "C101",
ptr: new(optionalAndTailField),
value: optionalAndTailField{A: 1},
},
{
input: "C20102",
ptr: new(optionalAndTailField),
value: optionalAndTailField{A: 1, B: 2, Tail: []uint{}},
},
{
input: "C401020304",
ptr: new(optionalAndTailField),
value: optionalAndTailField{A: 1, B: 2, Tail: []uint{3, 4}},
},
{
input: "C101",
ptr: new(optionalBigIntField),
value: optionalBigIntField{A: 1, B: nil},
},
{
input: "C20102",
ptr: new(optionalBigIntField),
value: optionalBigIntField{A: 1, B: big.NewInt(2)},
},
{
input: "C101",
ptr: new(optionalPtrField),
value: optionalPtrField{A: 1},
},
{
input: "C20180", // not accepted because "optional" doesn't enable "nil"
ptr: new(optionalPtrField),
error: "rlp: input string too short for [3]uint8, decoding into (rlp.optionalPtrField).B",
},
{
input: "C20102",
ptr: new(optionalPtrField),
error: "rlp: input string too short for [3]uint8, decoding into (rlp.optionalPtrField).B",
},
{
input: "C50183010203",
ptr: new(optionalPtrField),
value: optionalPtrField{A: 1, B: &[3]byte{1, 2, 3}},
},
{
input: "C101",
ptr: new(optionalPtrFieldNil),
value: optionalPtrFieldNil{A: 1},
},
{
input: "C20180", // accepted because "nil" tag allows empty input
ptr: new(optionalPtrFieldNil),
value: optionalPtrFieldNil{A: 1},
},
{
input: "C20102",
ptr: new(optionalPtrFieldNil),
error: "rlp: input string too short for [3]uint8, decoding into (rlp.optionalPtrFieldNil).B",
},
// struct tag "optional" field clearing
{
input: "C101",
ptr: &optionalFields{A: 9, B: 8, C: 7},
value: optionalFields{A: 1, B: 0, C: 0},
},
{
input: "C20102",
ptr: &optionalFields{A: 9, B: 8, C: 7},
value: optionalFields{A: 1, B: 2, C: 0},
},
{
input: "C20102",
ptr: &optionalAndTailField{A: 9, B: 8, Tail: []uint{7, 6, 5}},
value: optionalAndTailField{A: 1, B: 2, Tail: []uint{}},
},
{
input: "C101",
ptr: &optionalPtrField{A: 9, B: &[3]byte{8, 7, 6}},
value: optionalPtrField{A: 1},
},
// RawValue
{input: "01", ptr: new(RawValue), value: RawValue(unhex("01"))},
{input: "82FFFF", ptr: new(RawValue), value: RawValue(unhex("82FFFF"))},
@ -849,6 +980,40 @@ func TestDecoderFunc(t *testing.T) {
x()
}
// This tests the validity checks for fields with struct tag "optional".
func TestInvalidOptionalField(t *testing.T) {
type (
invalid1 struct {
A uint `rlp:"optional"`
B uint
}
invalid2 struct {
T []uint `rlp:"tail,optional"`
}
invalid3 struct {
T []uint `rlp:"optional,tail"`
}
)
tests := []struct {
v interface{}
err string
}{
{v: new(invalid1), err: `rlp: struct field rlp.invalid1.B needs "optional" tag`},
{v: new(invalid2), err: `rlp: invalid struct tag "optional" for rlp.invalid2.T (also has "tail" tag)`},
{v: new(invalid3), err: `rlp: invalid struct tag "tail" for rlp.invalid3.T (also has "optional" tag)`},
}
for _, test := range tests {
err := DecodeBytes(unhex("C20102"), test.v)
if err == nil {
t.Errorf("no error for %T", test.v)
} else if err.Error() != test.err {
t.Errorf("wrong error for %T: %v", test.v, err.Error())
}
}
}
func ExampleDecode() {
input, _ := hex.DecodeString("C90A1486666F6F626172")

View File

@ -102,29 +102,60 @@ Signed integers, floating point numbers, maps, channels and functions cannot be
Struct Tags
Package rlp honours certain struct tags: "-", "tail", "nil", "nilList" and "nilString".
As with other encoding packages, the "-" tag ignores fields.
The "-" tag ignores fields.
The "tail" tag, which may only be used on the last exported struct field, allows slurping
up any excess list elements into a slice. See examples for more details.
The "nil" tag applies to pointer-typed fields and changes the decoding rules for the field
such that input values of size zero decode as a nil pointer. This tag can be useful when
decoding recursive types.
type StructWithOptionalFoo struct {
Foo *[20]byte `rlp:"nil"`
type StructWithIgnoredField struct{
Ignored uint `rlp:"-"`
Field uint
}
Go struct values encode/decode as RLP lists. There are two ways of influencing the mapping
of fields to list elements. The "tail" tag, which may only be used on the last exported
struct field, allows slurping up any excess list elements into a slice.
type StructWithTail struct{
Field uint
Tail []string `rlp:"tail"`
}
The "optional" tag says that the field may be omitted if it is zero-valued. If this tag is
used on a struct field, all subsequent public fields must also be declared optional.
When encoding a struct with optional fields, the output RLP list contains all values up to
the last non-zero optional field.
When decoding into a struct, optional fields may be omitted from the end of the input
list. For the example below, this means input lists of one, two, or three elements are
accepted.
type StructWithOptionalFields struct{
Required uint
Optional1 uint `rlp:"optional"`
Optional2 uint `rlp:"optional"`
}
The "nil", "nilList" and "nilString" tags apply to pointer-typed fields only, and change
the decoding rules for the field type. For regular pointer fields without the "nil" tag,
input values must always match the required input length exactly and the decoder does not
produce nil values. When the "nil" tag is set, input values of size zero decode as a nil
pointer. This is especially useful for recursive types.
type StructWithNilField struct {
Field *[3]byte `rlp:"nil"`
}
In the example above, Field allows two possible input sizes. For input 0xC180 (a list
containing an empty string) Field is set to nil after decoding. For input 0xC483000000 (a
list containing a 3-byte string), Field is set to a non-nil array pointer.
RLP supports two kinds of empty values: empty lists and empty strings. When using the
"nil" tag, the kind of empty value allowed for a type is chosen automatically. A struct
field whose Go type is a pointer to an unsigned integer, string, boolean or byte
array/slice expects an empty RLP string. Any other pointer field type encodes/decodes as
an empty RLP list.
"nil" tag, the kind of empty value allowed for a type is chosen automatically. A field
whose Go type is a pointer to an unsigned integer, string, boolean or byte array/slice
expects an empty RLP string. Any other pointer field type encodes/decodes as an empty RLP
list.
The choice of null value can be made explicit with the "nilList" and "nilString" struct
tags. Using these tags encodes/decodes a Go nil pointer value as the kind of empty
RLP value defined by the tag.
tags. Using these tags encodes/decodes a Go nil pointer value as the empty RLP value kind
defined by the tag.
*/
package rlp

View File

@ -601,15 +601,40 @@ func makeStructWriter(typ reflect.Type) (writer, error) {
return nil, structFieldError{typ, f.index, f.info.writerErr}
}
}
writer := func(val reflect.Value, w *encbuf) error {
lh := w.list()
for _, f := range fields {
if err := f.info.writer(val.Field(f.index), w); err != nil {
return err
var writer writer
firstOptionalField := firstOptionalField(fields)
if firstOptionalField == len(fields) {
// This is the writer function for structs without any optional fields.
writer = func(val reflect.Value, w *encbuf) error {
lh := w.list()
for _, f := range fields {
if err := f.info.writer(val.Field(f.index), w); err != nil {
return err
}
}
w.listEnd(lh)
return nil
}
} else {
// If there are any "optional" fields, the writer needs to perform additional
// checks to determine the output list length.
writer = func(val reflect.Value, w *encbuf) error {
lastField := len(fields) - 1
for ; lastField >= firstOptionalField; lastField-- {
if !val.Field(fields[lastField].index).IsZero() {
break
}
}
lh := w.list()
for i := 0; i <= lastField; i++ {
if err := fields[i].info.writer(val.Field(fields[i].index), w); err != nil {
return err
}
}
w.listEnd(lh)
return nil
}
w.listEnd(lh)
return nil
}
return writer, nil
}

View File

@ -279,12 +279,30 @@ var encTests = []encTest{
{val: simplestruct{A: 3, B: "foo"}, output: "C50383666F6F"},
{val: &recstruct{5, nil}, output: "C205C0"},
{val: &recstruct{5, &recstruct{4, &recstruct{3, nil}}}, output: "C605C404C203C0"},
{val: &intField{X: 3}, error: "rlp: type int is not RLP-serializable (struct field rlp.intField.X)"},
// struct tag "-"
{val: &ignoredField{A: 1, B: 2, C: 3}, output: "C20103"},
// struct tag "tail"
{val: &tailRaw{A: 1, Tail: []RawValue{unhex("02"), unhex("03")}}, output: "C3010203"},
{val: &tailRaw{A: 1, Tail: []RawValue{unhex("02")}}, output: "C20102"},
{val: &tailRaw{A: 1, Tail: []RawValue{}}, output: "C101"},
{val: &tailRaw{A: 1, Tail: nil}, output: "C101"},
{val: &hasIgnoredField{A: 1, B: 2, C: 3}, output: "C20103"},
{val: &intField{X: 3}, error: "rlp: type int is not RLP-serializable (struct field rlp.intField.X)"},
// struct tag "optional"
{val: &optionalFields{}, output: "C180"},
{val: &optionalFields{A: 1}, output: "C101"},
{val: &optionalFields{A: 1, B: 2}, output: "C20102"},
{val: &optionalFields{A: 1, B: 2, C: 3}, output: "C3010203"},
{val: &optionalFields{A: 1, B: 0, C: 3}, output: "C3018003"},
{val: &optionalAndTailField{A: 1}, output: "C101"},
{val: &optionalAndTailField{A: 1, B: 2}, output: "C20102"},
{val: &optionalAndTailField{A: 1, Tail: []uint{5, 6}}, output: "C401800506"},
{val: &optionalAndTailField{A: 1, Tail: []uint{5, 6}}, output: "C401800506"},
{val: &optionalBigIntField{A: 1}, output: "C101"},
{val: &optionalPtrField{A: 1}, output: "C101"},
{val: &optionalPtrFieldNil{A: 1}, output: "C101"},
// nil
{val: (*uint)(nil), output: "80"},

View File

@ -38,15 +38,16 @@ type typeinfo struct {
// tags represents struct tags.
type tags struct {
// rlp:"nil" controls whether empty input results in a nil pointer.
nilOK bool
// This controls whether nil pointers are encoded/decoded as empty strings
// or empty lists.
// nilKind is the kind of empty value allowed for the field.
nilKind Kind
nilOK bool
// rlp:"tail" controls whether this field swallows additional list
// elements. It can only be set for the last field, which must be
// of slice type.
// rlp:"optional" allows for a field to be missing in the input list.
// If this is set, all subsequent fields must also be optional.
optional bool
// rlp:"tail" controls whether this field swallows additional list elements. It can
// only be set for the last field, which must be of slice type.
tail bool
// rlp:"-" ignores fields.
@ -104,28 +105,51 @@ func cachedTypeInfo1(typ reflect.Type, tags tags) *typeinfo {
}
type field struct {
index int
info *typeinfo
index int
info *typeinfo
optional bool
}
// structFields resolves the typeinfo of all public fields in a struct type.
func structFields(typ reflect.Type) (fields []field, err error) {
lastPublic := lastPublicField(typ)
var (
lastPublic = lastPublicField(typ)
anyOptional = false
)
for i := 0; i < typ.NumField(); i++ {
if f := typ.Field(i); f.PkgPath == "" { // exported
t, err := parseStructTag(typ, i, lastPublic)
tags, err := parseStructTag(typ, i, lastPublic)
if err != nil {
return nil, err
}
if t.ignored {
// Skip rlp:"-" fields.
if tags.ignored {
continue
}
info := cachedTypeInfo1(f.Type, t)
fields = append(fields, field{i, info})
// If any field has the "optional" tag, subsequent fields must also have it.
if tags.optional || tags.tail {
anyOptional = true
} else if anyOptional {
return nil, fmt.Errorf(`rlp: struct field %v.%s needs "optional" tag`, typ, f.Name)
}
info := cachedTypeInfo1(f.Type, tags)
fields = append(fields, field{i, info, tags.optional})
}
}
return fields, nil
}
// anyOptionalFields returns the index of the first field with "optional" tag.
func firstOptionalField(fields []field) int {
for i, f := range fields {
if f.optional {
return i
}
}
return len(fields)
}
type structFieldError struct {
typ reflect.Type
field int
@ -166,11 +190,19 @@ func parseStructTag(typ reflect.Type, fi, lastPublic int) (tags, error) {
case "nilList":
ts.nilKind = List
}
case "optional":
ts.optional = true
if ts.tail {
return ts, structTagError{typ, f.Name, t, `also has "tail" tag`}
}
case "tail":
ts.tail = true
if fi != lastPublic {
return ts, structTagError{typ, f.Name, t, "must be on last field"}
}
if ts.optional {
return ts, structTagError{typ, f.Name, t, `also has "optional" tag`}
}
if f.Type.Kind() != reflect.Slice {
return ts, structTagError{typ, f.Name, t, "field type is not slice"}
}