Skip to content

Commit

Permalink
ssz, tests: support operating on zero values across the board
Browse files Browse the repository at this point in the history
  • Loading branch information
karalabe committed Aug 30, 2024
1 parent 00f49d0 commit c74f127
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 60 deletions.
12 changes: 6 additions & 6 deletions cmd/sszgen/opset.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ func (p *parseContext) resolveBasicOpset(typ *types.Basic, tags *sizeTag, pointe
}, nil
} else {
return &opsetStatic{
"DefineUint64Ptr({{.Codec}}, &{{.Field}})",
"EncodeUint64Ptr({{.Codec}}, &{{.Field}})",
"DecodeUint64Ptr({{.Codec}}, &{{.Field}})",
"DefineUint64Pointer({{.Codec}}, &{{.Field}})",
"EncodeUint64Pointer({{.Codec}}, &{{.Field}})",
"DecodeUint64Pointer({{.Codec}}, &{{.Field}})",
[]int{8},
}, nil
}
Expand Down Expand Up @@ -199,9 +199,9 @@ func (p *parseContext) resolveArrayOpset(typ types.Type, size int, tags *sizeTag
}, nil
} else {
return &opsetStatic{
"DefineStaticBytesPtr({{.Codec}}, &{{.Field}})",
"EncodeStaticBytesPtr({{.Codec}}, &{{.Field}})",
"DecodeStaticBytesPtr({{.Codec}}, &{{.Field}})",
"DefineStaticBytesPointer({{.Codec}}, &{{.Field}})",
"EncodeStaticBytesPointer({{.Codec}}, &{{.Field}})",
"DecodeStaticBytesPointer({{.Codec}}, &{{.Field}})",
[]int{size},
}, nil

Expand Down
24 changes: 12 additions & 12 deletions codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,17 @@ func DefineUint64[T ~uint64](c *Codec, n *T) {
HashUint64(c.has, *n)
}

// DefineUint64Ptr defines the next field as a uint64.
func DefineUint64Ptr[T ~uint64](c *Codec, n **T) {
// DefineUint64Pointer defines the next field as a uint64.
func DefineUint64Pointer[T ~uint64](c *Codec, n **T) {
if c.enc != nil {
EncodeUint64Ptr(c.enc, *n)
EncodeUint64Pointer(c.enc, *n)
return
}
if c.dec != nil {
DecodeUint64Ptr(c.dec, n)
DecodeUint64Pointer(c.dec, n)
return
}
HashUint64Ptr(c.has, *n)
HashUint64Pointer(c.has, *n)
}

// DefineUint256 defines the next field as a uint256.
Expand Down Expand Up @@ -178,26 +178,26 @@ func DefineStaticBytes[T commonBytesLengths](c *Codec, blob *T) {
HashStaticBytes(c.has, blob)
}

// DefineStaticBytesPtr defines the next field as static binary blob. This method
// DefineStaticBytesPointer defines the next field as static binary blob. This method
// can be used for byte arrays.
func DefineStaticBytesPtr[T commonBytesLengths](c *Codec, blob **T) {
func DefineStaticBytesPointer[T commonBytesLengths](c *Codec, blob **T) {
if c.enc != nil {
EncodeStaticBytesPtr(c.enc, *blob)
EncodeStaticBytesPointer(c.enc, *blob)
return
}
if c.dec != nil {
DecodeStaticBytesPtr(c.dec, blob)
DecodeStaticBytesPointer(c.dec, blob)
return
}
HashStaticBytesPtr(c.has, *blob)
HashStaticBytesPointer(c.has, *blob)
}

// DefineCheckedStaticBytes defines the next field as static binary blob. This
// method can be used for plain byte slices, which is more expensive, since it
// needs runtime size validation.
func DefineCheckedStaticBytes(c *Codec, blob *[]byte, size uint64) {
if c.enc != nil {
EncodeCheckedStaticBytes(c.enc, *blob)
EncodeCheckedStaticBytes(c.enc, *blob, size)
return
}
if c.dec != nil {
Expand Down Expand Up @@ -385,7 +385,7 @@ func DefineUnsafeArrayOfStaticBytes[T commonBytesLengths](c *Codec, blobs []T) {
// which is more expensive since it needs runtime size validation.
func DefineCheckedArrayOfStaticBytes[T commonBytesLengths](c *Codec, blobs *[]T, size uint64) {
if c.enc != nil {
EncodeCheckedArrayOfStaticBytes(c.enc, *blobs)
EncodeCheckedArrayOfStaticBytes(c.enc, *blobs, size)
return
}
if c.dec != nil {
Expand Down
16 changes: 11 additions & 5 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,11 @@ func DecodeUint64[T ~uint64](dec *Decoder, n *T) {
}
}

// DecodeUint64Ptr parses a uint64.
func DecodeUint64Ptr[T ~uint64](dec *Decoder, n **T) {
// DecodeUint64Pointer parses a uint64.
//
// This method is similar to DecodeUint64, but will also initialize the pointer
// if it is not allocated yet.
func DecodeUint64Pointer[T ~uint64](dec *Decoder, n **T) {
if dec.err != nil {
return
}
Expand Down Expand Up @@ -295,8 +298,11 @@ func DecodeStaticBytes[T commonBytesLengths](dec *Decoder, blob *T) {
}
}

// DecodeStaticBytesPtr parses a static binary blob.
func DecodeStaticBytesPtr[T commonBytesLengths](dec *Decoder, blob **T) {
// DecodeStaticBytesPointer parses a static binary blob.
//
// This method is similar to DecodeStaticBytes, but will also initialize the
// pointer if it is not allocated yet.
func DecodeStaticBytesPointer[T commonBytesLengths](dec *Decoder, blob **T) {
if dec.err != nil {
return
}
Expand Down Expand Up @@ -933,7 +939,7 @@ func (dec *Decoder) decodeOffset(list bool) {
dec.offsets = append(dec.offsets, offset)
}

// retrieveSize retrieves the length of the nest dynamic item based on the seen
// retrieveSize retrieves the length of the next dynamic item based on the seen
// and cached offsets.
func (dec *Decoder) retrieveSize() uint32 {
// If sizes aren't yet available, pre-compute them all. The reason we use a
Expand Down
117 changes: 98 additions & 19 deletions encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/binary"
"io"
"math/big"
"reflect"
"unsafe"

"github.com/holiman/uint256"
Expand All @@ -19,6 +20,7 @@ var (
boolFalse = []byte{0x00}
boolTrue = []byte{0x01}
uint256Zero = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
bitlistZero = bitfield.NewBitlist(0)
)

// Encoder is a wrapper around an io.Writer or a []byte buffer to implement SSZ
Expand Down Expand Up @@ -161,10 +163,10 @@ func EncodeUint64[T ~uint64](enc *Encoder, n T) {
}
}

// EncodeUint64Ptr serializes a uint64.
// EncodeUint64Pointer serializes a uint64.
//
// Note, a nil pointer is serialized as zero.
func EncodeUint64Ptr[T ~uint64](enc *Encoder, n *T) {
func EncodeUint64Pointer[T ~uint64](enc *Encoder, n *T) {
if enc.outWriter != nil {
if enc.err != nil {
return
Expand Down Expand Up @@ -256,24 +258,24 @@ func EncodeStaticBytes[T commonBytesLengths](enc *Encoder, blob *T) {
}
}

// EncodeStaticBytesPtr serializes a static binary blob.
// EncodeStaticBytesPointer serializes a static binary blob.
//
// Note, a nil pointer is serialized as a zero-value blob.
func EncodeStaticBytesPtr[T commonBytesLengths](enc *Encoder, blob *T) {
func EncodeStaticBytesPointer[T commonBytesLengths](enc *Encoder, blob *T) {
// If the blob is nil, write a batch of zeroes and exit
if blob == nil {
enc.encodeZeroes(reflect.TypeFor[T]().Len())
return
}
// Blob not nil, write the actual data content
if enc.outWriter != nil {
if enc.err != nil {
return
}
if blob == nil {
blob = new(T) // TODO(karalabe): Make this alloc free somehow?
}
// The code below should have used `*blob[:]`, alas Go's generics compiler
// is missing that (i.e. a bug): https://github.com/golang/go/issues/51740
_, enc.err = enc.outWriter.Write(unsafe.Slice(&(*blob)[0], len(*blob)))
} else {
if blob == nil {
blob = new(T) // TODO(karalabe): Make this alloc free somehow?
}
// The code below should have used `blob[:]`, alas Go's generics compiler
// is missing that (i.e. a bug): https://github.com/golang/go/issues/51740
copy(enc.outBuffer, unsafe.Slice(&(*blob)[0], len(*blob)))
Expand All @@ -282,7 +284,13 @@ func EncodeStaticBytesPtr[T commonBytesLengths](enc *Encoder, blob *T) {
}

// EncodeCheckedStaticBytes serializes a static binary blob.
func EncodeCheckedStaticBytes(enc *Encoder, blob []byte) {
func EncodeCheckedStaticBytes(enc *Encoder, blob []byte, size uint64) {
// If the blob is nil, write a batch of zeroes and exit
if blob == nil {
enc.encodeZeroes(int(size))
return
}
// Blob not nil, write the actual data content
if enc.outWriter != nil {
if enc.err != nil {
return
Expand Down Expand Up @@ -323,15 +331,22 @@ func EncodeDynamicBytesContent(enc *Encoder, blob []byte) {
}

// EncodeStaticObject serializes a static ssz object.
func EncodeStaticObject(enc *Encoder, obj StaticObject) {
func EncodeStaticObject[T newableStaticObject[U], U any](enc *Encoder, obj T) {
if enc.err != nil {
return
}
if obj == nil {
// If the object is nil, pull up it's zero value. This will be very slow,
// but it should not happen in production, only during tests mostly.
obj = zeroValueStatic[T, U]()
}
obj.DefineSSZ(enc.codec)
}

// EncodeDynamicObjectOffset serializes a dynamic ssz object.
func EncodeDynamicObjectOffset(enc *Encoder, obj DynamicObject) {
//
// Note, nil will be encoded as a zero-value initialized object.
func EncodeDynamicObjectOffset[T newableDynamicObject[U], U any](enc *Encoder, obj T) {
if enc.outWriter != nil {
if enc.err != nil {
return
Expand All @@ -342,14 +357,26 @@ func EncodeDynamicObjectOffset(enc *Encoder, obj DynamicObject) {
binary.LittleEndian.PutUint32(enc.outBuffer, enc.offset)
enc.outBuffer = enc.outBuffer[4:]
}
// If the object is nil, pull up it's zero value. This will be very slow, but
// it should not happen in production, only during tests mostly.
if obj == nil {
obj = zeroValueDynamic[T, U]()
}
enc.offset += obj.SizeSSZ(enc.sizer, false)
}

// EncodeDynamicObjectContent is the lazy data writer for EncodeDynamicObjectOffset.
func EncodeDynamicObjectContent(enc *Encoder, obj DynamicObject) {
//
// Note, nil will be encoded as a zero-value initialized object.
func EncodeDynamicObjectContent[T newableDynamicObject[U], U any](enc *Encoder, obj T) {
if enc.err != nil {
return
}
// If the object is nil, pull up it's zero value. This will be very slow, but
// it should not happen in production, only during tests mostly.
if obj == nil {
obj = zeroValueDynamic[T, U]()
}
enc.offsetDynamics(obj.SizeSSZ(enc.sizer, true))
obj.DefineSSZ(enc.codec)
}
Expand All @@ -372,6 +399,8 @@ func EncodeArrayOfBits[T commonBitsLengths](enc *Encoder, bits *T) {
}

// EncodeSliceOfBitsOffset serializes a dynamic slice of (packed) bits.
//
// Note, a nil slice of bits is serialized as an empty bit list.
func EncodeSliceOfBitsOffset(enc *Encoder, bits bitfield.Bitlist) {
if enc.outWriter != nil {
if enc.err != nil {
Expand All @@ -383,19 +412,34 @@ func EncodeSliceOfBitsOffset(enc *Encoder, bits bitfield.Bitlist) {
binary.LittleEndian.PutUint32(enc.outBuffer, enc.offset)
enc.outBuffer = enc.outBuffer[4:]
}
enc.offset += uint32(len(bits))
if bits != nil {
enc.offset += uint32(len(bits))
} else {
enc.offset += uint32(len(bitlistZero))
}
}

// EncodeSliceOfBitsContent is the lazy data writer for EncodeSliceOfBitsOffset.
//
// Note, a nil slice of bits is serialized as an empty bit list.
func EncodeSliceOfBitsContent(enc *Encoder, bits bitfield.Bitlist) {
if enc.outWriter != nil {
if enc.err != nil {
return
}
_, enc.err = enc.outWriter.Write(bits) // bitfield.Bitlist already has the length bit set
if bits != nil {
_, enc.err = enc.outWriter.Write(bits) // bitfield.Bitlist already has the length bit set
} else {
_, enc.err = enc.outWriter.Write(bitlistZero)
}
} else {
copy(enc.outBuffer, bits)
enc.outBuffer = enc.outBuffer[len(bits):] // bitfield.Bitlist already has the length bit set
if bits != nil {
copy(enc.outBuffer, bits)
enc.outBuffer = enc.outBuffer[len(bits):] // bitfield.Bitlist already has the length bit set
} else {
copy(enc.outBuffer, bitlistZero)
enc.outBuffer = enc.outBuffer[len(bitlistZero):]
}
}
}

Expand Down Expand Up @@ -502,7 +546,12 @@ func EncodeUnsafeArrayOfStaticBytes[T commonBytesLengths](enc *Encoder, blobs []

// EncodeCheckedArrayOfStaticBytes serializes a static array of static binary
// blobs.
func EncodeCheckedArrayOfStaticBytes[T commonBytesLengths](enc *Encoder, blobs []T) {
func EncodeCheckedArrayOfStaticBytes[T commonBytesLengths](enc *Encoder, blobs []T, size uint64) {
// If the blobs are nil, write a batch of zeroes and exit
if blobs == nil {
enc.encodeZeroes(int(size) * reflect.TypeFor[T]().Len())
return
}
// Internally this method is essentially calling EncodeStaticBytes on all
// the blobs in a loop. Practically, we've inlined that call to make things
// a *lot* faster.
Expand Down Expand Up @@ -721,3 +770,33 @@ func EncodeSliceOfDynamicObjectsContent[T DynamicObject](enc *Encoder, objects [
func (enc *Encoder) offsetDynamics(offset uint32) {
enc.offset = offset
}

// encodeZeroes is a helper to append a bunch of zero values to the output stream.
// This method is mainly used for encoding uninitialized fields without allocating
// them beforehand.
func (enc *Encoder) encodeZeroes(size int) {
if enc.outWriter != nil {
if enc.err != nil {
return
}
for size >= 32 {
if _, enc.err = enc.outWriter.Write(uint256Zero); enc.err != nil {
return
}
size -= 32
}
if size > 0 {
_, enc.err = enc.outWriter.Write(uint256Zero[:size])
}
} else {
for size >= 32 {
copy(enc.outBuffer, uint256Zero)
enc.outBuffer = enc.outBuffer[32:]
size -= 32
}
if size > 0 {
copy(enc.outBuffer, uint256Zero[:size])
enc.outBuffer = enc.outBuffer[size:]
}
}
}
Loading

0 comments on commit c74f127

Please sign in to comment.