diff --git a/spectests/BUILD.bazel b/spectests/BUILD.bazel index e36579df..c42b2be2 100644 --- a/spectests/BUILD.bazel +++ b/spectests/BUILD.bazel @@ -28,6 +28,7 @@ go_test( tags = ["spectest"], deps = [ "//:go_default_library", + "//types:go_default_library", "@com_github_ghodss_yaml//:go_default_library", "@io_bazel_rules_go//go/tools/bazel:go_default_library", ], diff --git a/spectests/ssz_spec_test.go b/spectests/ssz_spec_test.go index 912c727c..6d9afaf9 100644 --- a/spectests/ssz_spec_test.go +++ b/spectests/ssz_spec_test.go @@ -12,8 +12,13 @@ import ( "github.com/bazelbuild/rules_go/go/tools/bazel" "github.com/ghodss/yaml" "github.com/prysmaticlabs/go-ssz" + "github.com/prysmaticlabs/go-ssz/types" ) +func init() { + types.ToggleCache(false) +} + // sszComparisonConfig is used to specify the value to marshal, unmarshal into, // as well as the expected results from the spec test YAML files. type sszComparisonConfig struct { diff --git a/ssz.go b/ssz.go index 0fd89915..a28071fe 100644 --- a/ssz.go +++ b/ssz.go @@ -133,7 +133,7 @@ func HashTreeRoot(val interface{}) ([32]byte, error) { if err != nil { return [32]byte{}, errors.Wrapf(err, "could not generate tree hasher for type: %v", rval.Type()) } - return factory.Root(rval, rval.Type(), 0) + return factory.Root(rval, rval.Type(), "", 0) } // HashTreeRootBitlist determines the root hash of a bitfield.Bitlist type using SSZ's Merkleization. @@ -162,7 +162,7 @@ func HashTreeRootWithCapacity(val interface{}, maxCapacity uint64) ([32]byte, er if err != nil { return [32]byte{}, errors.Wrapf(err, "could not generate tree hasher for type: %v", rval.Type()) } - return factory.Root(rval, rval.Type(), maxCapacity) + return factory.Root(rval, rval.Type(), "", maxCapacity) } // SigningRoot truncates the last property of the struct passed in diff --git a/ssz_test.go b/ssz_test.go index d5ab00f7..75c457a8 100644 --- a/ssz_test.go +++ b/ssz_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/hex" "reflect" + "strconv" "sync" "testing" @@ -12,6 +13,10 @@ import ( "github.com/prysmaticlabs/go-ssz/types" ) +type beaconState struct { + BlockRoots [][]byte `ssz-size:"65536,32"` +} + type fork struct { PreviousVersion [4]byte CurrentVersion [4]byte @@ -694,6 +699,74 @@ func TestSigningRoot_ConcurrentAccess(t *testing.T) { wg.Wait() } +func BenchmarkSSZ_NoCache(b *testing.B) { + b.StopTimer() + bs := &beaconState{ + BlockRoots: make([][]byte, 65536), + } + for i := 0; i < len(bs.BlockRoots); i++ { + newItem := [32]byte{1, 2, 3} + bs.BlockRoots[i] = newItem[:] + } + b.StartTimer() + for i := 0; i < b.N; i++ { + if _, err := HashTreeRoot(bs); err != nil { + b.Fatal(err) + } + } + types.ToggleCache(true) +} + +func BenchmarkSSZ_WithCache(b *testing.B) { + b.StopTimer() + types.ToggleCache(true) + bs := &beaconState{ + BlockRoots: make([][]byte, 65536), + } + for i := 0; i < len(bs.BlockRoots); i++ { + newItem := [32]byte{1, 2, 3} + bs.BlockRoots[i] = newItem[:] + } + b.StartTimer() + for i := 0; i < b.N; i++ { + if _, err := HashTreeRoot(bs); err != nil { + b.Fatal(err) + } + } + types.ToggleCache(false) +} + +func BenchmarkSSZ_SingleElementChanged(b *testing.B) { + b.StopTimer() + types.ToggleCache(true) + bs := &beaconState{ + BlockRoots: make([][]byte, 65536), + } + for i := 0; i < len(bs.BlockRoots); i++ { + newItem := [32]byte{1, 2, 3} + bs.BlockRoots[i] = newItem[:] + } + if _, err := HashTreeRoot(bs); err != nil { + b.Fatal(err) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + newItem := []byte(strconv.Itoa(i)) + newRoot := toBytes32(newItem) + bs.BlockRoots[i%len(bs.BlockRoots)] = newRoot[:] + if _, err := HashTreeRoot(bs); err != nil { + b.Fatal(err) + } + } + types.ToggleCache(false) +} + +func toBytes32(x []byte) [32]byte { + var y [32]byte + copy(y[:], x) + return y +} + func hexDecodeOrDie(t *testing.T, s string) []byte { res, err := hex.DecodeString(s) if err != nil { diff --git a/types/BUILD.bazel b/types/BUILD.bazel index 0fcd0a16..f0ed74f9 100644 --- a/types/BUILD.bazel +++ b/types/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "array_basic.go", "array_composite.go", + "array_roots.go", "basic.go", "determine_size.go", "factory.go", @@ -30,7 +31,7 @@ go_library( go_test( name = "go_default_test", srcs = [ - "array_basic_test.go", + "array_roots_test.go", "helpers_test.go", "struct_test.go", ], diff --git a/types/array_basic.go b/types/array_basic.go index 9d34f9e2..94ae98fd 100644 --- a/types/array_basic.go +++ b/types/array_basic.go @@ -25,31 +25,18 @@ func newBasicArraySSZ() *basicArraySSZ { } } -func (b *basicArraySSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity uint64) ([32]byte, error) { +func (b *basicArraySSZ) Root(val reflect.Value, typ reflect.Type, fieldName string, maxCapacity uint64) ([32]byte, error) { numItems := val.Len() hashKeyElements := make([]byte, BytesPerChunk*numItems) emptyKey := highwayhash.Sum(hashKeyElements, fastSumHashKey[:]) leaves := make([][]byte, numItems) - elemKind := typ.Elem().Kind() offset := 0 - var factory SSZAble - var err error - if numItems > 0 { - factory, err = SSZFactory(val.Index(0), typ.Elem()) - if err != nil { - return [32]byte{}, err - } + factory, err := SSZFactory(val.Index(0), typ.Elem()) + if err != nil { + return [32]byte{}, err } for i := 0; i < numItems; i++ { - // If we are marshaling an byte array of length 32, we shortcut the computations and - // simply return it as an identity root. - if elemKind == reflect.Array && typ.Elem().Elem().Kind() == reflect.Uint8 && val.Index(i).Len() == 32 { - leaves[i] = val.Index(i).Bytes() - copy(hashKeyElements[offset:offset+32], leaves[i]) - offset += 32 - continue - } - r, err := factory.Root(val.Index(i), typ.Elem(), 0) + r, err := factory.Root(val.Index(i), typ.Elem(), "", 0) if err != nil { return [32]byte{}, err } @@ -58,7 +45,7 @@ func (b *basicArraySSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity ui offset += 32 } hashKey := highwayhash.Sum(hashKeyElements, fastSumHashKey[:]) - if hashKey != emptyKey { + if enableCache && hashKey != emptyKey { b.lock.Lock() res := b.hashCache.Get(string(hashKey[:])) b.lock.Unlock() @@ -74,7 +61,7 @@ func (b *basicArraySSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity ui if err != nil { return [32]byte{}, err } - if hashKey != emptyKey { + if enableCache && hashKey != emptyKey { b.lock.Lock() b.hashCache.Set(string(hashKey[:]), root, time.Hour) b.lock.Unlock() diff --git a/types/array_basic_test.go b/types/array_basic_test.go deleted file mode 100644 index 093cf599..00000000 --- a/types/array_basic_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package types - -import ( - "reflect" - "strconv" - "testing" -) - -func BenchmarkBasicArrayRoot_WithCache(b *testing.B) { - b.StopTimer() - items := [65536][]byte{} - for i := 0; i < len(items); i++ { - items[i] = make([]byte, 32) - copy(items[i], strconv.Itoa(i)) - } - ss := newBasicArraySSZ() - v := reflect.ValueOf(items) - typ := reflect.TypeOf([65536][32]byte{}) - b.StartTimer() - for i := 0; i < b.N; i++ { - if _, err := ss.Root(v, typ, 0); err != nil { - b.Fatal(err) - } - } -} diff --git a/types/array_composite.go b/types/array_composite.go index ba55e125..e6b6c6db 100644 --- a/types/array_composite.go +++ b/types/array_composite.go @@ -11,7 +11,7 @@ func newCompositeArraySSZ() *compositeArraySSZ { return &compositeArraySSZ{} } -func (b *compositeArraySSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity uint64) ([32]byte, error) { +func (b *compositeArraySSZ) Root(val reflect.Value, typ reflect.Type, fieldName string, maxCapacity uint64) ([32]byte, error) { var factory SSZAble var err error numItems := val.Len() @@ -30,7 +30,7 @@ func (b *compositeArraySSZ) Root(val reflect.Value, typ reflect.Type, maxCapacit } limit := (uint64(val.Len())*elemSize + 31) / 32 for i := 0; i < val.Len(); i++ { - r, err := factory.Root(val.Index(i), typ.Elem(), 0) + r, err := factory.Root(val.Index(i), typ.Elem(), "", 0) if err != nil { return [32]byte{}, err } diff --git a/types/array_roots.go b/types/array_roots.go new file mode 100644 index 00000000..8d37ead1 --- /dev/null +++ b/types/array_roots.go @@ -0,0 +1,195 @@ +package types + +import ( + "bytes" + "fmt" + "reflect" + "sync" + "time" + + "github.com/karlseguin/ccache" + "github.com/minio/highwayhash" + "github.com/protolambda/zssz/merkle" +) + +// RootsArraySizeCache for hash tree root. +const RootsArraySizeCache = 100000 + +type rootsArraySSZ struct { + hashCache *ccache.Cache + lock sync.Mutex + cachedLeaves map[string][][]byte + layers map[string][][][]byte +} + +func newRootsArraySSZ() *rootsArraySSZ { + return &rootsArraySSZ{ + hashCache: ccache.New(ccache.Configure().MaxSize(RootsArraySizeCache)), + cachedLeaves: make(map[string][][]byte), + layers: make(map[string][][][]byte), + } +} + +func (a *rootsArraySSZ) Root(val reflect.Value, typ reflect.Type, fieldName string, maxCapacity uint64) ([32]byte, error) { + numItems := typ.Len() + // We make sure to look into the cache only if a field name is provided, that is, + // if this function is called when calling HashTreeRoot on a struct type that has + // a field which is an array of roots. An example is: + // + // type BeaconState struct { + // BlockRoots [2048][32]byte + // } + // + // which would allow us to look into the cache by the field "BlockRoots". + if enableCache && fieldName != "" { + if _, ok := a.layers[fieldName]; !ok { + depth := merkle.GetDepth(uint64(numItems)) + a.layers[fieldName] = make([][][]byte, depth+1) + } + } + hashKeyElements := make([]byte, BytesPerChunk*numItems) + emptyKey := highwayhash.Sum(hashKeyElements, fastSumHashKey[:]) + offset := 0 + leaves := make([][]byte, numItems) + changedIndices := make([]int, 0) + for i := 0; i < numItems; i++ { + var item [32]byte + if res, ok := val.Index(i).Interface().([32]byte); ok { + item = res + } else if res, ok := val.Index(i).Interface().([]byte); ok { + item = toBytes32(res) + } else { + return [32]byte{}, fmt.Errorf("expected array or slice of len 32, received %v", val.Index(i)) + } + leaves[i] = item[:] + copy(hashKeyElements[offset:offset+32], leaves[i]) + offset += 32 + if enableCache && fieldName != "" { + if _, ok := a.cachedLeaves[fieldName]; ok { + if !bytes.Equal(leaves[i], a.cachedLeaves[fieldName][i]) { + changedIndices = append(changedIndices, i) + } + } + } + } + chunks := leaves + // Recompute the root from the modified branches from the previous call + // to this function. + if len(changedIndices) > 0 { + var rt [32]byte + for i := 0; i < len(changedIndices); i++ { + rt = a.recomputeRoot(changedIndices[i], chunks, fieldName) + } + return rt, nil + } + hashKey := highwayhash.Sum(hashKeyElements, fastSumHashKey[:]) + if enableCache && hashKey != emptyKey { + a.lock.Lock() + res := a.hashCache.Get(string(hashKey[:])) + a.lock.Unlock() + if res != nil && res.Value() != nil { + return res.Value().([32]byte), nil + } + } + root := a.merkleize(chunks, fieldName) + if enableCache && fieldName != "" { + a.cachedLeaves[fieldName] = leaves + } + if enableCache && hashKey != emptyKey { + a.lock.Lock() + a.hashCache.Set(string(hashKey[:]), root, time.Hour) + a.lock.Unlock() + } + return root, nil +} + +func (a *rootsArraySSZ) Marshal(val reflect.Value, typ reflect.Type, buf []byte, startOffset uint64) (uint64, error) { + index := startOffset + if val.Len() == 0 { + return index, nil + } + for i := 0; i < val.Len(); i++ { + var item [32]byte + if res, ok := val.Index(i).Interface().([32]byte); ok { + item = res + } else if res, ok := val.Index(i).Interface().([]byte); ok { + item = toBytes32(res) + } else { + return 0, fmt.Errorf("expected array or slice of len 32, received %v", val.Index(i)) + } + copy(buf[index:index+uint64(len(item))], item[:]) + index += uint64(len(item)) + } + return index, nil +} + +func (a *rootsArraySSZ) Unmarshal(val reflect.Value, typ reflect.Type, input []byte, startOffset uint64) (uint64, error) { + i := 0 + index := startOffset + for i < val.Len() { + val.Index(i).SetBytes(input[index : index+uint64(32)]) + index += uint64(32) + i++ + } + return index, nil +} + +func (a *rootsArraySSZ) recomputeRoot(idx int, chunks [][]byte, fieldName string) [32]byte { + root := chunks[idx] + for i := 0; i < len(a.layers[fieldName])-1; i++ { + subIndex := (uint64(idx) / (1 << uint64(i))) ^ 1 + isLeft := uint64(idx) / (1 << uint64(i)) + parentIdx := uint64(idx) / (1 << uint64(i+1)) + item := a.layers[fieldName][i][subIndex] + if isLeft%2 != 0 { + parentHash := hash(append(item, root...)) + root = parentHash[:] + } else { + parentHash := hash(append(root, item...)) + root = parentHash[:] + } + // Update the cached layers at the parent index. + a.layers[fieldName][i+1][parentIdx] = root + } + return toBytes32(root) +} + +func (a *rootsArraySSZ) merkleize(chunks [][]byte, fieldName string) [32]byte { + if len(chunks) == 1 { + var root [32]byte + copy(root[:], chunks[0]) + return root + } + for !isPowerOf2(len(chunks)) { + chunks = append(chunks, make([]byte, BytesPerChunk)) + } + hashLayer := chunks + if enableCache && fieldName != "" { + a.layers[fieldName][0] = hashLayer + } + // We keep track of the hash layers of a Merkle trie until we reach + // the top layer of length 1, which contains the single root element. + // [Root] -> Top layer has length 1. + // [E] [F] -> This layer has length 2. + // [A] [B] [C] [D] -> The bottom layer has length 4 (needs to be a power of two). + i := 1 + for len(hashLayer) > 1 { + layer := [][]byte{} + for i := 0; i < len(hashLayer); i += 2 { + hashedChunk := hash(append(hashLayer[i], hashLayer[i+1]...)) + layer = append(layer, hashedChunk[:]) + } + hashLayer = layer + if enableCache && fieldName != "" { + a.layers[fieldName][i] = hashLayer + } + i++ + } + var root [32]byte + copy(root[:], hashLayer[0]) + return root +} + +func isPowerOf2(n int) bool { + return n != 0 && (n&(n-1)) == 0 +} diff --git a/types/array_roots_test.go b/types/array_roots_test.go new file mode 100644 index 00000000..8a8623fb --- /dev/null +++ b/types/array_roots_test.go @@ -0,0 +1,49 @@ +package types + +import ( + "reflect" + "testing" +) + +type beaconState struct { + BlockRoots [65536][32]byte +} + +func BenchmarkRootsArray_Root_WithCache(b *testing.B) { + b.StopTimer() + bs := beaconState{ + BlockRoots: [65536][32]byte{}, + } + for i := 0; i < len(bs.BlockRoots); i++ { + bs.BlockRoots[i] = [32]byte{1, 2, 3} + } + ss := newRootsArraySSZ() + v := reflect.ValueOf(bs.BlockRoots) + typ := v.Type() + b.StartTimer() + for i := 0; i < b.N; i++ { + if _, err := ss.Root(v, typ, "BlockRoots", 0); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkRootsArray_Root_MinimalChanges(b *testing.B) { + b.StopTimer() + bs := beaconState{ + BlockRoots: [65536][32]byte{}, + } + for i := 0; i < len(bs.BlockRoots); i++ { + bs.BlockRoots[i] = [32]byte{1, 2, 3} + } + ss := newRootsArraySSZ() + v := reflect.ValueOf(bs.BlockRoots) + typ := v.Type() + b.StartTimer() + for i := 0; i < b.N; i++ { + bs.BlockRoots[i%len(bs.BlockRoots)] = [32]byte{4, 5, 6} + if _, err := ss.Root(v, typ, "BlockRoots", 0); err != nil { + b.Fatal(err) + } + } +} diff --git a/types/basic.go b/types/basic.go index d27b99d3..2158139c 100644 --- a/types/basic.go +++ b/types/basic.go @@ -103,7 +103,7 @@ func BitlistRoot(bfield bitfield.Bitlist, maxCapacity uint64) ([32]byte, error) return mixInLength(root, output), nil } -func (b *basicSSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity uint64) ([32]byte, error) { +func (b *basicSSZ) Root(val reflect.Value, typ reflect.Type, fieldName string, maxCapacity uint64) ([32]byte, error) { var chunks [][]byte var err error var hashKey string diff --git a/types/determine_size.go b/types/determine_size.go index c8414ded..0a5c5fe6 100644 --- a/types/determine_size.go +++ b/types/determine_size.go @@ -33,6 +33,13 @@ func isBasicTypeArray(typ reflect.Type, kind reflect.Kind) bool { return kind == reflect.Array && isBasicType(typ.Elem().Kind()) } +func isRootsArray(val reflect.Value, typ reflect.Type) bool { + elemTyp := typ.Elem() + elemKind := elemTyp.Kind() + isByteArray := elemKind == reflect.Array && elemTyp.Elem().Kind() == reflect.Uint8 + return isByteArray && val.Index(0).Len() == 32 +} + func isVariableSizeType(typ reflect.Type) bool { kind := typ.Kind() switch { diff --git a/types/factory.go b/types/factory.go index db0fe723..7b7246d7 100644 --- a/types/factory.go +++ b/types/factory.go @@ -5,12 +5,20 @@ import ( "reflect" ) +var enableCache = false + +// ToggleCache enables caching of ssz hash tree root. It is disabled by default. +func ToggleCache(val bool) { + enableCache = val +} + // StructFactory exports an implementation of a interface // containing helpers for marshaling/unmarshaling, and determining // the hash tree root of struct values. var StructFactory = newStructSSZ() var basicFactory = newBasicSSZ() var basicArrayFactory = newBasicArraySSZ() +var rootsArrayFactory = newRootsArraySSZ() var compositeArrayFactory = newCompositeArraySSZ() var basicSliceFactory = newBasicSliceSSZ() var stringFactory = newStringSSZ() @@ -20,7 +28,7 @@ var compositeSliceFactory = newCompositeSliceSSZ() // hash tree root according to the Simple Serialize specification. // See: https://github.com/ethereum/eth2.0-specs/blob/v0.8.2/specs/simple-serialize.md. type SSZAble interface { - Root(val reflect.Value, typ reflect.Type, maxCapacity uint64) ([32]byte, error) + Root(val reflect.Value, typ reflect.Type, fieldName string, maxCapacity uint64) ([32]byte, error) Marshal(val reflect.Value, typ reflect.Type, buf []byte, startOffset uint64) (uint64, error) Unmarshal(val reflect.Value, typ reflect.Type, buf []byte, startOffset uint64) (uint64, error) } @@ -47,6 +55,8 @@ func SSZFactory(val reflect.Value, typ reflect.Type) (SSZAble, error) { } case kind == reflect.Array: switch { + case isRootsArray(val, typ): + return rootsArrayFactory, nil case isBasicTypeArray(typ.Elem(), typ.Elem().Kind()): return basicArrayFactory, nil case !isVariableSizeType(typ.Elem()): diff --git a/types/slice_basic.go b/types/slice_basic.go index c96997e4..4a2919e2 100644 --- a/types/slice_basic.go +++ b/types/slice_basic.go @@ -12,7 +12,7 @@ func newBasicSliceSSZ() *basicSliceSSZ { return &basicSliceSSZ{} } -func (b *basicSliceSSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity uint64) ([32]byte, error) { +func (b *basicSliceSSZ) Root(val reflect.Value, typ reflect.Type, fieldName string, maxCapacity uint64) ([32]byte, error) { var factory SSZAble var limit uint64 var elemSize uint64 @@ -47,7 +47,7 @@ func (b *basicSliceSSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity ui } leaves[i] = innerBuf } else { - r, err := factory.Root(val.Index(i), typ.Elem(), 0) + r, err := factory.Root(val.Index(i), typ.Elem(), fieldName, 0) if err != nil { return [32]byte{}, err } diff --git a/types/slice_composite.go b/types/slice_composite.go index 6dd3ebfc..b63c12b0 100644 --- a/types/slice_composite.go +++ b/types/slice_composite.go @@ -12,7 +12,7 @@ func newCompositeSliceSSZ() *compositeSliceSSZ { return &compositeSliceSSZ{} } -func (b *compositeSliceSSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity uint64) ([32]byte, error) { +func (b *compositeSliceSSZ) Root(val reflect.Value, typ reflect.Type, fieldName string, maxCapacity uint64) ([32]byte, error) { output := make([]byte, 32) if val.Len() == 0 && maxCapacity == 0 { root, err := bitwiseMerkleize([][]byte{}, 0, 0) @@ -32,7 +32,7 @@ func (b *compositeSliceSSZ) Root(val reflect.Value, typ reflect.Type, maxCapacit } roots := make([][]byte, numItems) for i := 0; i < numItems; i++ { - r, err := factory.Root(val.Index(i), typ.Elem(), 0) + r, err := factory.Root(val.Index(i), typ.Elem(), fieldName, 0) if err != nil { return [32]byte{}, err } diff --git a/types/string.go b/types/string.go index c9467052..22c5727e 100644 --- a/types/string.go +++ b/types/string.go @@ -12,7 +12,7 @@ func newStringSSZ() *stringSSZ { return &stringSSZ{} } -func (b *stringSSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity uint64) ([32]byte, error) { +func (b *stringSSZ) Root(val reflect.Value, typ reflect.Type, fieldName string, maxCapacity uint64) ([32]byte, error) { var err error numItems := val.Len() elemSize := uint64(1) diff --git a/types/struct.go b/types/struct.go index 9b02e51b..a1a2ddb3 100644 --- a/types/struct.go +++ b/types/struct.go @@ -22,13 +22,13 @@ func newStructSSZ() *structSSZ { return &structSSZ{} } -func (b *structSSZ) Root(val reflect.Value, typ reflect.Type, maxCapacity uint64) ([32]byte, error) { +func (b *structSSZ) Root(val reflect.Value, typ reflect.Type, fieldName string, maxCapacity uint64) ([32]byte, error) { if typ.Kind() == reflect.Ptr { if val.IsNil() { instance := reflect.New(typ.Elem()).Elem() - return b.Root(instance, instance.Type(), maxCapacity) + return b.Root(instance, instance.Type(), fieldName, maxCapacity) } - return b.Root(val.Elem(), typ.Elem(), maxCapacity) + return b.Root(val.Elem(), typ.Elem(), fieldName, maxCapacity) } numFields := typ.NumField() return b.FieldsHasher(val, typ, numFields) @@ -61,7 +61,7 @@ func (b *structSSZ) FieldsHasher(val reflect.Value, typ reflect.Type, numFields if err != nil { return [32]byte{}, err } - r, err := factory.Root(val.Field(i), fType, fCapacity) + r, err := factory.Root(val.Field(i), fType, typ.Field(i).Name, fCapacity) if err != nil { return [32]byte{}, err }