From 7950a249266a692551e5a910adb9a82a02c92040 Mon Sep 17 00:00:00 2001 From: terence Date: Fri, 19 Dec 2025 23:29:42 -0500 Subject: [PATCH] feat(primitives): add BuilderIndex SSZ type (#16169) This pr adds primitives.BuilderIndex for builder registry indexing in Gloas --- changelog/builder-index.md | 3 + consensus-types/primitives/BUILD.bazel | 2 + consensus-types/primitives/builder_index.go | 54 ++++++++++++ .../primitives/builder_index_test.go | 86 +++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 changelog/builder-index.md create mode 100644 consensus-types/primitives/builder_index.go create mode 100644 consensus-types/primitives/builder_index_test.go diff --git a/changelog/builder-index.md b/changelog/builder-index.md new file mode 100644 index 0000000000..32dfa896a3 --- /dev/null +++ b/changelog/builder-index.md @@ -0,0 +1,3 @@ +### Added + +- `primitives.BuilderIndex`: SSZ `uint64` wrapper for builder registry indices. \ No newline at end of file diff --git a/consensus-types/primitives/BUILD.bazel b/consensus-types/primitives/BUILD.bazel index 295326cc2f..8a8a7089f1 100644 --- a/consensus-types/primitives/BUILD.bazel +++ b/consensus-types/primitives/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "basis_points.go", + "builder_index.go", "committee_bits_mainnet.go", "committee_bits_minimal.go", # keep "committee_index.go", @@ -31,6 +32,7 @@ go_library( go_test( name = "go_default_test", srcs = [ + "builder_index_test.go", "committee_index_test.go", "domain_test.go", "epoch_test.go", diff --git a/consensus-types/primitives/builder_index.go b/consensus-types/primitives/builder_index.go new file mode 100644 index 0000000000..4685838a10 --- /dev/null +++ b/consensus-types/primitives/builder_index.go @@ -0,0 +1,54 @@ +package primitives + +import ( + "fmt" + + fssz "github.com/prysmaticlabs/fastssz" +) + +var _ fssz.HashRoot = (BuilderIndex)(0) +var _ fssz.Marshaler = (*BuilderIndex)(nil) +var _ fssz.Unmarshaler = (*BuilderIndex)(nil) + +// BuilderIndex is an index into the builder registry. +type BuilderIndex uint64 + +// HashTreeRoot returns the SSZ hash tree root of the index. +func (b BuilderIndex) HashTreeRoot() ([32]byte, error) { + return fssz.HashWithDefaultHasher(b) +} + +// HashTreeRootWith appends the SSZ uint64 representation of the index to the given hasher. +func (b BuilderIndex) HashTreeRootWith(hh *fssz.Hasher) error { + hh.PutUint64(uint64(b)) + return nil +} + +// UnmarshalSSZ decodes the SSZ-encoded uint64 index from buf. +func (b *BuilderIndex) UnmarshalSSZ(buf []byte) error { + if len(buf) != b.SizeSSZ() { + return fmt.Errorf("expected buffer of length %d received %d", b.SizeSSZ(), len(buf)) + } + *b = BuilderIndex(fssz.UnmarshallUint64(buf)) + return nil +} + +// MarshalSSZTo appends the SSZ-encoded index to dst and returns the extended buffer. +func (b *BuilderIndex) MarshalSSZTo(dst []byte) ([]byte, error) { + marshalled, err := b.MarshalSSZ() + if err != nil { + return nil, err + } + return append(dst, marshalled...), nil +} + +// MarshalSSZ encodes the index as an SSZ uint64. +func (b *BuilderIndex) MarshalSSZ() ([]byte, error) { + marshalled := fssz.MarshalUint64([]byte{}, uint64(*b)) + return marshalled, nil +} + +// SizeSSZ returns the size of the SSZ-encoded index in bytes. +func (b *BuilderIndex) SizeSSZ() int { + return 8 +} diff --git a/consensus-types/primitives/builder_index_test.go b/consensus-types/primitives/builder_index_test.go new file mode 100644 index 0000000000..c2a67a38a3 --- /dev/null +++ b/consensus-types/primitives/builder_index_test.go @@ -0,0 +1,86 @@ +package primitives_test + +import ( + "encoding/binary" + "slices" + "strconv" + "testing" + + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + "github.com/OffchainLabs/prysm/v7/testing/require" +) + +func TestBuilderIndex_SSZRoundTripAndHashRoot(t *testing.T) { + cases := []uint64{ + 0, + 1, + 42, + (1 << 32) - 1, + 1 << 32, + ^uint64(0), + } + + for _, v := range cases { + t.Run("v="+u64name(v), func(t *testing.T) { + t.Parallel() + + val := primitives.BuilderIndex(v) + require.Equal(t, 8, (&val).SizeSSZ()) + + enc, err := (&val).MarshalSSZ() + require.NoError(t, err) + require.Equal(t, 8, len(enc)) + + wantEnc := make([]byte, 8) + binary.LittleEndian.PutUint64(wantEnc, v) + require.DeepEqual(t, wantEnc, enc) + + dstPrefix := []byte("prefix:") + dst, err := (&val).MarshalSSZTo(slices.Clone(dstPrefix)) + require.NoError(t, err) + wantDst := append(dstPrefix, wantEnc...) + require.DeepEqual(t, wantDst, dst) + + var decoded primitives.BuilderIndex + require.NoError(t, (&decoded).UnmarshalSSZ(enc)) + require.Equal(t, val, decoded) + + root, err := val.HashTreeRoot() + require.NoError(t, err) + + var wantRoot [32]byte + binary.LittleEndian.PutUint64(wantRoot[:8], v) + require.Equal(t, wantRoot, root) + }) + } +} + +func TestBuilderIndex_UnmarshalSSZRejectsWrongSize(t *testing.T) { + for _, size := range []int{7, 9} { + t.Run("size="+strconv.Itoa(size), func(t *testing.T) { + t.Parallel() + var v primitives.BuilderIndex + err := (&v).UnmarshalSSZ(make([]byte, size)) + require.ErrorContains(t, "expected buffer of length 8", err) + }) + } +} + +func u64name(v uint64) string { + switch v { + case 0: + return "0" + case 1: + return "1" + case 42: + return "42" + case (1 << 32) - 1: + return "2^32-1" + case 1 << 32: + return "2^32" + case ^uint64(0): + return "max" + default: + return "custom" + } +}