Skip to content

Commit

Permalink
rlp/rlpgen: RLP encoder code generator (ethereum#24251)
Browse files Browse the repository at this point in the history
This change adds a code generator tool for creating EncodeRLP method
implementations. The generated methods will behave identically to the
reflect-based encoder, but run faster because there is no reflection overhead.

Package rlp now provides the EncoderBuffer type for incremental encoding. This
is used by generated code, but the new methods can also be useful for
hand-written encoders.

There is also experimental support for generating DecodeRLP, and some new
methods have been added to the existing Stream type to support this. Creating
decoders with rlpgen is not recommended at this time because the generated
methods create very poor error reporting.

More detail about package rlp changes:

* rlp: externalize struct field processing / validation

This adds a new package, rlp/internal/rlpstruct, in preparation for the
RLP encoder generator.

I think the struct field rules are subtle enough to warrant extracting
this into their own package, even though it means that a bunch of
adapter code is needed for converting to/from rlpstruct.Type.

* rlp: add more decoder methods (for rlpgen)

This adds new methods on rlp.Stream:

- Uint64, Uint32, Uint16, Uint8, BigInt
- ReadBytes for decoding into []byte
- MoreDataInList - useful for optional list elements

* rlp: expose encoder buffer (for rlpgen)

This exposes the internal encoder buffer type for use in EncodeRLP
implementations.

The new EncoderBuffer type is a sort-of 'opaque handle' for a pointer to
encBuffer. It is implemented this way to ensure the global encBuffer pool
is handled correctly.
  • Loading branch information
fjl authored Feb 16, 2022
1 parent 4335bbb commit 9b93564
Show file tree
Hide file tree
Showing 25 changed files with 2,693 additions and 466 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ require (
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912
golang.org/x/text v0.3.6
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
golang.org/x/tools v0.1.0
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce
gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6
gopkg.in/urfave/cli.v1 v1.20.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -580,6 +581,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
180 changes: 131 additions & 49 deletions rlp/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
"reflect"
"strings"
"sync"

"github.com/ethereum/go-ethereum/rlp/internal/rlpstruct"
)

//lint:ignore ST1012 EOL is not an error.
Expand Down Expand Up @@ -148,7 +150,7 @@ var (
bigInt = reflect.TypeOf(big.Int{})
)

func makeDecoder(typ reflect.Type, tags tags) (dec decoder, err error) {
func makeDecoder(typ reflect.Type, tags rlpstruct.Tags) (dec decoder, err error) {
kind := typ.Kind()
switch {
case typ == rawValueType:
Expand Down Expand Up @@ -220,63 +222,28 @@ func decodeBigIntNoPtr(s *Stream, val reflect.Value) error {
}

func decodeBigInt(s *Stream, val reflect.Value) error {
var buffer []byte
kind, size, err := s.Kind()
switch {
case err != nil:
return wrapStreamError(err, val.Type())
case kind == List:
return wrapStreamError(ErrExpectedString, val.Type())
case kind == Byte:
buffer = s.uintbuf[:1]
buffer[0] = s.byteval
s.kind = -1 // re-arm Kind
case size == 0:
// Avoid zero-length read.
s.kind = -1
case size <= uint64(len(s.uintbuf)):
// For integers smaller than s.uintbuf, allocating a buffer
// can be avoided.
buffer = s.uintbuf[:size]
if err := s.readFull(buffer); err != nil {
return wrapStreamError(err, val.Type())
}
// Reject inputs where single byte encoding should have been used.
if size == 1 && buffer[0] < 128 {
return wrapStreamError(ErrCanonSize, val.Type())
}
default:
// For large integers, a temporary buffer is needed.
buffer = make([]byte, size)
if err := s.readFull(buffer); err != nil {
return wrapStreamError(err, val.Type())
}
}

// Reject leading zero bytes.
if len(buffer) > 0 && buffer[0] == 0 {
return wrapStreamError(ErrCanonInt, val.Type())
}

// Set the integer bytes.
i := val.Interface().(*big.Int)
if i == nil {
i = new(big.Int)
val.Set(reflect.ValueOf(i))
}
i.SetBytes(buffer)

err := s.decodeBigInt(i)
if err != nil {
return wrapStreamError(err, val.Type())
}
return nil
}

func makeListDecoder(typ reflect.Type, tag tags) (decoder, error) {
func makeListDecoder(typ reflect.Type, tag rlpstruct.Tags) (decoder, error) {
etype := typ.Elem()
if etype.Kind() == reflect.Uint8 && !reflect.PtrTo(etype).Implements(decoderInterface) {
if typ.Kind() == reflect.Array {
return decodeByteArray, nil
}
return decodeByteSlice, nil
}
etypeinfo := theTC.infoWhileGenerating(etype, tags{})
etypeinfo := theTC.infoWhileGenerating(etype, rlpstruct.Tags{})
if etypeinfo.decoderErr != nil {
return nil, etypeinfo.decoderErr
}
Expand All @@ -286,7 +253,7 @@ func makeListDecoder(typ reflect.Type, tag tags) (decoder, error) {
dec = func(s *Stream, val reflect.Value) error {
return decodeListArray(s, val, etypeinfo.decoder)
}
case tag.tail:
case tag.Tail:
// A slice with "tail" tag can occur as the last field
// of a struct and is supposed to swallow all remaining
// list elements. The struct decoder already called s.List,
Expand Down Expand Up @@ -451,16 +418,16 @@ func zeroFields(structval reflect.Value, fields []field) {
}

// makePtrDecoder creates a decoder that decodes into the pointer's element type.
func makePtrDecoder(typ reflect.Type, tag tags) (decoder, error) {
func makePtrDecoder(typ reflect.Type, tag rlpstruct.Tags) (decoder, error) {
etype := typ.Elem()
etypeinfo := theTC.infoWhileGenerating(etype, tags{})
etypeinfo := theTC.infoWhileGenerating(etype, rlpstruct.Tags{})
switch {
case etypeinfo.decoderErr != nil:
return nil, etypeinfo.decoderErr
case !tag.nilOK:
case !tag.NilOK:
return makeSimplePtrDecoder(etype, etypeinfo), nil
default:
return makeNilPtrDecoder(etype, etypeinfo, tag.nilKind), nil
return makeNilPtrDecoder(etype, etypeinfo, tag), nil
}
}

Expand All @@ -481,9 +448,13 @@ func makeSimplePtrDecoder(etype reflect.Type, etypeinfo *typeinfo) decoder {
// values are decoded into a value of the element type, just like makePtrDecoder does.
//
// This decoder is used for pointer-typed struct fields with struct tag "nil".
func makeNilPtrDecoder(etype reflect.Type, etypeinfo *typeinfo, nilKind Kind) decoder {
func makeNilPtrDecoder(etype reflect.Type, etypeinfo *typeinfo, ts rlpstruct.Tags) decoder {
typ := reflect.PtrTo(etype)
nilPtr := reflect.Zero(typ)

// Determine the value kind that results in nil pointer.
nilKind := typeNilKind(etype, ts)

return func(s *Stream, val reflect.Value) (err error) {
kind, size, err := s.Kind()
if err != nil {
Expand Down Expand Up @@ -659,6 +630,37 @@ func (s *Stream) Bytes() ([]byte, error) {
}
}

// ReadBytes decodes the next RLP value and stores the result in b.
// The value size must match len(b) exactly.
func (s *Stream) ReadBytes(b []byte) error {
kind, size, err := s.Kind()
if err != nil {
return err
}
switch kind {
case Byte:
if len(b) != 1 {
return fmt.Errorf("input value has wrong size 1, want %d", len(b))
}
b[0] = s.byteval
s.kind = -1 // rearm Kind
return nil
case String:
if uint64(len(b)) != size {
return fmt.Errorf("input value has wrong size %d, want %d", size, len(b))
}
if err = s.readFull(b); err != nil {
return err
}
if size == 1 && b[0] < 128 {
return ErrCanonSize
}
return nil
default:
return ErrExpectedString
}
}

// Raw reads a raw encoded value including RLP type information.
func (s *Stream) Raw() ([]byte, error) {
kind, size, err := s.Kind()
Expand Down Expand Up @@ -687,10 +689,31 @@ func (s *Stream) Raw() ([]byte, error) {
// Uint reads an RLP string of up to 8 bytes and returns its contents
// as an unsigned integer. If the input does not contain an RLP string, the
// returned error will be ErrExpectedString.
//
// Deprecated: use s.Uint64 instead.
func (s *Stream) Uint() (uint64, error) {
return s.uint(64)
}

func (s *Stream) Uint64() (uint64, error) {
return s.uint(64)
}

func (s *Stream) Uint32() (uint32, error) {
i, err := s.uint(32)
return uint32(i), err
}

func (s *Stream) Uint16() (uint16, error) {
i, err := s.uint(16)
return uint16(i), err
}

func (s *Stream) Uint8() (uint8, error) {
i, err := s.uint(8)
return uint8(i), err
}

func (s *Stream) uint(maxbits int) (uint64, error) {
kind, size, err := s.Kind()
if err != nil {
Expand Down Expand Up @@ -781,6 +804,65 @@ func (s *Stream) ListEnd() error {
return nil
}

// MoreDataInList reports whether the current list context contains
// more data to be read.
func (s *Stream) MoreDataInList() bool {
_, listLimit := s.listLimit()
return listLimit > 0
}

// BigInt decodes an arbitrary-size integer value.
func (s *Stream) BigInt() (*big.Int, error) {
i := new(big.Int)
if err := s.decodeBigInt(i); err != nil {
return nil, err
}
return i, nil
}

func (s *Stream) decodeBigInt(dst *big.Int) error {
var buffer []byte
kind, size, err := s.Kind()
switch {
case err != nil:
return err
case kind == List:
return ErrExpectedString
case kind == Byte:
buffer = s.uintbuf[:1]
buffer[0] = s.byteval
s.kind = -1 // re-arm Kind
case size == 0:
// Avoid zero-length read.
s.kind = -1
case size <= uint64(len(s.uintbuf)):
// For integers smaller than s.uintbuf, allocating a buffer
// can be avoided.
buffer = s.uintbuf[:size]
if err := s.readFull(buffer); err != nil {
return err
}
// Reject inputs where single byte encoding should have been used.
if size == 1 && buffer[0] < 128 {
return ErrCanonSize
}
default:
// For large integers, a temporary buffer is needed.
buffer = make([]byte, size)
if err := s.readFull(buffer); err != nil {
return err
}
}

// Reject leading zero bytes.
if len(buffer) > 0 && buffer[0] == 0 {
return ErrCanonInt
}
// Set the integer bytes.
dst.SetBytes(buffer)
return nil
}

// Decode decodes a value and stores the result in the value pointed
// to by val. Please see the documentation for the Decode function
// to learn about the decoding rules.
Expand Down
43 changes: 42 additions & 1 deletion rlp/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,47 @@ func TestStreamRaw(t *testing.T) {
}
}

func TestStreamReadBytes(t *testing.T) {
tests := []struct {
input string
size int
err string
}{
// kind List
{input: "C0", size: 1, err: "rlp: expected String or Byte"},
// kind Byte
{input: "04", size: 0, err: "input value has wrong size 1, want 0"},
{input: "04", size: 1},
{input: "04", size: 2, err: "input value has wrong size 1, want 2"},
// kind String
{input: "820102", size: 0, err: "input value has wrong size 2, want 0"},
{input: "820102", size: 1, err: "input value has wrong size 2, want 1"},
{input: "820102", size: 2},
{input: "820102", size: 3, err: "input value has wrong size 2, want 3"},
}

for _, test := range tests {
test := test
name := fmt.Sprintf("input_%s/size_%d", test.input, test.size)
t.Run(name, func(t *testing.T) {
s := NewStream(bytes.NewReader(unhex(test.input)), 0)
b := make([]byte, test.size)
err := s.ReadBytes(b)
if test.err == "" {
if err != nil {
t.Errorf("unexpected error %q", err)
}
} else {
if err == nil {
t.Errorf("expected error, got nil")
} else if err.Error() != test.err {
t.Errorf("wrong error %q", err)
}
}
})
}
}

func TestDecodeErrors(t *testing.T) {
r := bytes.NewReader(nil)

Expand Down Expand Up @@ -990,7 +1031,7 @@ func TestInvalidOptionalField(t *testing.T) {
v interface{}
err string
}{
{v: new(invalid1), err: `rlp: struct field rlp.invalid1.B needs "optional" tag`},
{v: new(invalid1), err: `rlp: invalid struct tag "" for rlp.invalid1.B (must be optional because preceding field "A" is optional)`},
{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)`},
}
Expand Down
Loading

0 comments on commit 9b93564

Please sign in to comment.