From 7185c4fb3903ea7c3070737074a689425e58c754 Mon Sep 17 00:00:00 2001 From: terence tsao Date: Thu, 9 Oct 2025 14:04:16 -0700 Subject: [PATCH] Add Gloas slot processing with execution payload availability updates --- beacon-chain/core/transition/transition.go | 12 +++ .../core/transition/transition_gloas_test.go | 100 ++++++++++++++++++ beacon-chain/state/BUILD.bazel | 1 + beacon-chain/state/interfaces.go | 1 + beacon-chain/state/interfaces_gloas.go | 6 ++ beacon-chain/state/state-native/BUILD.bazel | 1 + .../state/state-native/getters_state.go | 10 ++ .../state/state-native/setters_gloas.go | 30 ++++++ .../state/state-native/setters_gloas_test.go | 50 +++++++++ testing/spectest/mainnet/BUILD.bazel | 2 + .../mainnet/gloas__sanity__slots_test.go | 11 ++ testing/spectest/minimal/BUILD.bazel | 2 + .../minimal/gloas__sanity__slots_test.go | 11 ++ .../spectest/shared/gloas/sanity/BUILD.bazel | 19 ++++ .../shared/gloas/sanity/slot_processing.go | 61 +++++++++++ 15 files changed, 317 insertions(+) create mode 100644 beacon-chain/core/transition/transition_gloas_test.go create mode 100644 beacon-chain/state/interfaces_gloas.go create mode 100644 beacon-chain/state/state-native/setters_gloas.go create mode 100644 beacon-chain/state/state-native/setters_gloas_test.go create mode 100644 testing/spectest/mainnet/gloas__sanity__slots_test.go create mode 100644 testing/spectest/minimal/gloas__sanity__slots_test.go create mode 100644 testing/spectest/shared/gloas/sanity/BUILD.bazel create mode 100644 testing/spectest/shared/gloas/sanity/slot_processing.go diff --git a/beacon-chain/core/transition/transition.go b/beacon-chain/core/transition/transition.go index d9286dab75..257783534d 100644 --- a/beacon-chain/core/transition/transition.go +++ b/beacon-chain/core/transition/transition.go @@ -142,6 +142,18 @@ func ProcessSlot(ctx context.Context, state state.BeaconState) (state.BeaconStat ); err != nil { return nil, err } + + // Spec v1.6.1 (pseudocode): + // # [New in Gloas:EIP7732] + // # Unset the next payload availability + // state.execution_payload_availability[(state.slot + 1) % SLOTS_PER_HISTORICAL_ROOT] = 0b0 + if state.Version() >= version.Gloas { + index := uint64((state.Slot() + 1) % params.BeaconConfig().SlotsPerHistoricalRoot) + if err := state.UpdateExecutionPayloadAvailabilityAtIndex(index, 0x0); err != nil { + return nil, err + } + } + return state, nil } diff --git a/beacon-chain/core/transition/transition_gloas_test.go b/beacon-chain/core/transition/transition_gloas_test.go new file mode 100644 index 0000000000..32353cfe54 --- /dev/null +++ b/beacon-chain/core/transition/transition_gloas_test.go @@ -0,0 +1,100 @@ +package transition + +import ( + "context" + "errors" + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/state" + "github.com/OffchainLabs/prysm/v7/config/params" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/runtime/version" + "github.com/stretchr/testify/require" +) + +type trackingState struct { + state.BeaconState + slot primitives.Slot + version int + header *ethpb.BeaconBlockHeader + epaCalls int + epaIndex uint64 + epaValue byte + epaErr error +} + +func (s *trackingState) Slot() primitives.Slot { + return s.slot +} + +func (s *trackingState) Version() int { + return s.version +} + +func (s *trackingState) HashTreeRoot(_ context.Context) ([32]byte, error) { + return [32]byte{0x01}, nil +} + +func (s *trackingState) UpdateStateRootAtIndex(_ uint64, _ [32]byte) error { + return nil +} + +func (s *trackingState) LatestBlockHeader() *ethpb.BeaconBlockHeader { + return s.header +} + +func (s *trackingState) SetLatestBlockHeader(header *ethpb.BeaconBlockHeader) error { + s.header = header + return nil +} + +func (s *trackingState) UpdateBlockRootAtIndex(_ uint64, _ [32]byte) error { + return nil +} + +func (s *trackingState) UpdateExecutionPayloadAvailabilityAtIndex(idx uint64, val byte) error { + s.epaCalls++ + s.epaIndex = idx + s.epaValue = val + if s.epaErr != nil { + return s.epaErr + } + return nil +} + +func TestProcessSlot_GloasClearsNextPayloadAvailability(t *testing.T) { + st := &trackingState{ + slot: 10, + version: version.Gloas, + header: testBeaconBlockHeader(), + } + + _, err := ProcessSlot(context.Background(), st) + require.NoError(t, err) + require.Equal(t, 1, st.epaCalls) + require.Equal(t, uint64((st.slot+1)%params.BeaconConfig().SlotsPerHistoricalRoot), st.epaIndex) + require.Equal(t, byte(0x0), st.epaValue) +} + +func TestProcessSlot_GloasAvailabilityUpdateError(t *testing.T) { + updateErr := errors.New("update failed") + st := &trackingState{ + slot: 7, + version: version.Gloas, + header: testBeaconBlockHeader(), + epaErr: updateErr, + } + + _, err := ProcessSlot(context.Background(), st) + require.ErrorIs(t, err, updateErr) + require.Equal(t, 1, st.epaCalls) +} + +func testBeaconBlockHeader() *ethpb.BeaconBlockHeader { + return ðpb.BeaconBlockHeader{ + ParentRoot: make([]byte, 32), + StateRoot: make([]byte, 32), + BodyRoot: make([]byte, 32), + } +} diff --git a/beacon-chain/state/BUILD.bazel b/beacon-chain/state/BUILD.bazel index af0a4bc5ae..82b53b59b8 100644 --- a/beacon-chain/state/BUILD.bazel +++ b/beacon-chain/state/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "error.go", "interfaces.go", + "interfaces_gloas.go", "prometheus.go", ], importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/state", diff --git a/beacon-chain/state/interfaces.go b/beacon-chain/state/interfaces.go index c884a92d50..b5501a0444 100644 --- a/beacon-chain/state/interfaces.go +++ b/beacon-chain/state/interfaces.go @@ -98,6 +98,7 @@ type WriteOnlyBeaconState interface { WriteOnlyWithdrawals WriteOnlyDeposits WriteOnlyProposerLookahead + WriteOnlyGloas SetGenesisTime(val time.Time) error SetGenesisValidatorsRoot(val []byte) error SetSlot(val primitives.Slot) error diff --git a/beacon-chain/state/interfaces_gloas.go b/beacon-chain/state/interfaces_gloas.go new file mode 100644 index 0000000000..74082b8ebb --- /dev/null +++ b/beacon-chain/state/interfaces_gloas.go @@ -0,0 +1,6 @@ +package state + +// WriteOnlyGloas defines a struct which only has write access to Gloas field methods. +type WriteOnlyGloas interface { + UpdateExecutionPayloadAvailabilityAtIndex(idx uint64, val byte) error +} diff --git a/beacon-chain/state/state-native/BUILD.bazel b/beacon-chain/state/state-native/BUILD.bazel index 597d3e6496..90e56c5aa9 100644 --- a/beacon-chain/state/state-native/BUILD.bazel +++ b/beacon-chain/state/state-native/BUILD.bazel @@ -36,6 +36,7 @@ go_library( "setters_deposit_requests.go", "setters_deposits.go", "setters_eth1.go", + "setters_gloas.go", "setters_misc.go", "setters_participation.go", "setters_payload_header.go", diff --git a/beacon-chain/state/state-native/getters_state.go b/beacon-chain/state/state-native/getters_state.go index a6a3529f0c..2c2221dbd0 100644 --- a/beacon-chain/state/state-native/getters_state.go +++ b/beacon-chain/state/state-native/getters_state.go @@ -721,3 +721,13 @@ func ProtobufBeaconStateFulu(s any) (*ethpb.BeaconStateFulu, error) { } return pbState, nil } + +// ProtobufBeaconStateGloas transforms an input into beacon state Gloas in the form of protobuf. +// Error is returned if the input is not type protobuf beacon state. +func ProtobufBeaconStateGloas(s any) (*ethpb.BeaconStateGloas, error) { + pbState, ok := s.(*ethpb.BeaconStateGloas) + if !ok { + return nil, errors.New("input is not type pb.BeaconStateGloas") + } + return pbState, nil +} diff --git a/beacon-chain/state/state-native/setters_gloas.go b/beacon-chain/state/state-native/setters_gloas.go new file mode 100644 index 0000000000..8735ad8dba --- /dev/null +++ b/beacon-chain/state/state-native/setters_gloas.go @@ -0,0 +1,30 @@ +package state_native + +import ( + "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native/types" + "github.com/pkg/errors" +) + +// UpdateExecutionPayloadAvailabilityAtIndex updates the execution payload availability bit at a specific index. +func (b *BeaconState) UpdateExecutionPayloadAvailabilityAtIndex(idx uint64, val byte) error { + b.lock.Lock() + defer b.lock.Unlock() + + byteIndex := idx / 8 + bitIndex := idx % 8 + + if byteIndex >= uint64(len(b.executionPayloadAvailability)) { + return errors.Errorf("bit index %d (byte index %d) out of range for execution payload availability length %d", idx, byteIndex, len(b.executionPayloadAvailability)) + } + + if val != 0 { + // Set the bit + b.executionPayloadAvailability[byteIndex] |= (1 << bitIndex) + } else { + // Clear the bit + b.executionPayloadAvailability[byteIndex] &^= (1 << bitIndex) + } + + b.markFieldAsDirty(types.ExecutionPayloadAvailability) + return nil +} diff --git a/beacon-chain/state/state-native/setters_gloas_test.go b/beacon-chain/state/state-native/setters_gloas_test.go new file mode 100644 index 0000000000..1cae69fa68 --- /dev/null +++ b/beacon-chain/state/state-native/setters_gloas_test.go @@ -0,0 +1,50 @@ +package state_native + +import ( + "testing" + + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/stretchr/testify/require" +) + +func TestUpdateExecutionPayloadAvailabilityAtIndex_SetAndClear(t *testing.T) { + st := newGloasStateWithAvailability(t, make([]byte, 1024)) + + otherIdx := uint64(8) // byte 1, bit 0 + idx := uint64(9) // byte 1, bit 1 + + require.NoError(t, st.UpdateExecutionPayloadAvailabilityAtIndex(otherIdx, 1)) + require.Equal(t, byte(0x01), st.executionPayloadAvailability[1]) + + require.NoError(t, st.UpdateExecutionPayloadAvailabilityAtIndex(idx, 1)) + require.Equal(t, byte(0x03), st.executionPayloadAvailability[1]) + + require.NoError(t, st.UpdateExecutionPayloadAvailabilityAtIndex(idx, 0)) + require.Equal(t, byte(0x01), st.executionPayloadAvailability[1]) +} + +func TestUpdateExecutionPayloadAvailabilityAtIndex_OutOfRange(t *testing.T) { + st := newGloasStateWithAvailability(t, make([]byte, 1024)) + + idx := uint64(len(st.executionPayloadAvailability)) * 8 + err := st.UpdateExecutionPayloadAvailabilityAtIndex(idx, 1) + require.Error(t, err) + require.ErrorContains(t, err, "out of range") + + for _, b := range st.executionPayloadAvailability { + if b != 0 { + t.Fatalf("execution payload availability mutated on error") + } + } +} + +func newGloasStateWithAvailability(t *testing.T, availability []byte) *BeaconState { + t.Helper() + + st, err := InitializeFromProtoUnsafeGloas(ðpb.BeaconStateGloas{ + ExecutionPayloadAvailability: availability, + }) + require.NoError(t, err) + + return st.(*BeaconState) +} diff --git a/testing/spectest/mainnet/BUILD.bazel b/testing/spectest/mainnet/BUILD.bazel index 75d3340e6a..45f3286545 100644 --- a/testing/spectest/mainnet/BUILD.bazel +++ b/testing/spectest/mainnet/BUILD.bazel @@ -200,6 +200,7 @@ go_test( "fulu__sanity__blocks_test.go", "fulu__sanity__slots_test.go", "fulu__ssz_static__ssz_static_test.go", + "gloas__sanity__slots_test.go", "gloas__ssz_static__ssz_static_test.go", "phase0__epoch_processing__effective_balance_updates_test.go", "phase0__epoch_processing__epoch_processing_test.go", @@ -278,6 +279,7 @@ go_test( "//testing/spectest/shared/fulu/rewards:go_default_library", "//testing/spectest/shared/fulu/sanity:go_default_library", "//testing/spectest/shared/fulu/ssz_static:go_default_library", + "//testing/spectest/shared/gloas/sanity:go_default_library", "//testing/spectest/shared/gloas/ssz_static:go_default_library", "//testing/spectest/shared/phase0/epoch_processing:go_default_library", "//testing/spectest/shared/phase0/finality:go_default_library", diff --git a/testing/spectest/mainnet/gloas__sanity__slots_test.go b/testing/spectest/mainnet/gloas__sanity__slots_test.go new file mode 100644 index 0000000000..db55729e79 --- /dev/null +++ b/testing/spectest/mainnet/gloas__sanity__slots_test.go @@ -0,0 +1,11 @@ +package mainnet + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/sanity" +) + +func TestMainnet_Gloas_Sanity_Slots(t *testing.T) { + sanity.RunSlotProcessingTests(t, "mainnet") +} diff --git a/testing/spectest/minimal/BUILD.bazel b/testing/spectest/minimal/BUILD.bazel index 5f80e7f82d..f617c90499 100644 --- a/testing/spectest/minimal/BUILD.bazel +++ b/testing/spectest/minimal/BUILD.bazel @@ -206,6 +206,7 @@ go_test( "fulu__sanity__blocks_test.go", "fulu__sanity__slots_test.go", "fulu__ssz_static__ssz_static_test.go", + "gloas__sanity__slots_test.go", "gloas__ssz_static__ssz_static_test.go", "phase0__epoch_processing__effective_balance_updates_test.go", "phase0__epoch_processing__epoch_processing_test.go", @@ -288,6 +289,7 @@ go_test( "//testing/spectest/shared/fulu/rewards:go_default_library", "//testing/spectest/shared/fulu/sanity:go_default_library", "//testing/spectest/shared/fulu/ssz_static:go_default_library", + "//testing/spectest/shared/gloas/sanity:go_default_library", "//testing/spectest/shared/gloas/ssz_static:go_default_library", "//testing/spectest/shared/phase0/epoch_processing:go_default_library", "//testing/spectest/shared/phase0/finality:go_default_library", diff --git a/testing/spectest/minimal/gloas__sanity__slots_test.go b/testing/spectest/minimal/gloas__sanity__slots_test.go new file mode 100644 index 0000000000..c763a5c2f5 --- /dev/null +++ b/testing/spectest/minimal/gloas__sanity__slots_test.go @@ -0,0 +1,11 @@ +package minimal + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/sanity" +) + +func TestMinimal_Gloas_Sanity_Slots(t *testing.T) { + sanity.RunSlotProcessingTests(t, "minimal") +} diff --git a/testing/spectest/shared/gloas/sanity/BUILD.bazel b/testing/spectest/shared/gloas/sanity/BUILD.bazel new file mode 100644 index 0000000000..3144e5db9d --- /dev/null +++ b/testing/spectest/shared/gloas/sanity/BUILD.bazel @@ -0,0 +1,19 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + testonly = True, + srcs = ["slot_processing.go"], + importpath = "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/sanity", + visibility = ["//testing/spectest:__subpackages__"], + deps = [ + "//beacon-chain/core/transition:go_default_library", + "//beacon-chain/state/state-native:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//testing/require:go_default_library", + "//testing/spectest/utils:go_default_library", + "//testing/util:go_default_library", + "@com_github_golang_snappy//:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", + ], +) diff --git a/testing/spectest/shared/gloas/sanity/slot_processing.go b/testing/spectest/shared/gloas/sanity/slot_processing.go new file mode 100644 index 0000000000..e046033805 --- /dev/null +++ b/testing/spectest/shared/gloas/sanity/slot_processing.go @@ -0,0 +1,61 @@ +package sanity + +import ( + "context" + "strconv" + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition" + state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/require" + "github.com/OffchainLabs/prysm/v7/testing/spectest/utils" + "github.com/OffchainLabs/prysm/v7/testing/util" + "github.com/golang/snappy" + "google.golang.org/protobuf/proto" +) + +func init() { + transition.SkipSlotCache.Disable() +} + +// RunSlotProcessingTests executes "sanity/slots" tests. +func RunSlotProcessingTests(t *testing.T, config string) { + require.NoError(t, utils.SetConfig(t, config)) + + testFolders, testsFolderPath := utils.TestFolders(t, config, "gloas", "sanity/slots/pyspec_tests") + + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + preBeaconStateFile, err := util.BazelFileBytes(testsFolderPath, folder.Name(), "pre.ssz_snappy") + require.NoError(t, err) + preBeaconStateSSZ, err := snappy.Decode(nil /* dst */, preBeaconStateFile) + require.NoError(t, err, "Failed to decompress") + base := ðpb.BeaconStateGloas{} + require.NoError(t, base.UnmarshalSSZ(preBeaconStateSSZ), "Failed to unmarshal") + beaconState, err := state_native.InitializeFromProtoGloas(base) + require.NoError(t, err) + + file, err := util.BazelFileBytes(testsFolderPath, folder.Name(), "slots.yaml") + require.NoError(t, err) + fileStr := string(file) + slotsCount, err := strconv.ParseUint(fileStr[:len(fileStr)-5], 10, 64) + require.NoError(t, err) + + postBeaconStateFile, err := util.BazelFileBytes(testsFolderPath, folder.Name(), "post.ssz_snappy") + require.NoError(t, err) + postBeaconStateSSZ, err := snappy.Decode(nil /* dst */, postBeaconStateFile) + require.NoError(t, err, "Failed to decompress") + postBeaconState := ðpb.BeaconStateGloas{} + require.NoError(t, postBeaconState.UnmarshalSSZ(postBeaconStateSSZ), "Failed to unmarshal") + postState, err := transition.ProcessSlots(context.Background(), beaconState, beaconState.Slot().Add(slotsCount)) + require.NoError(t, err) + + pbState, err := state_native.ProtobufBeaconStateGloas(postState.ToProto()) + require.NoError(t, err) + if !proto.Equal(pbState, postBeaconState) { + t.Fatal("Did not receive expected post state") + } + }) + } +}