diff --git a/beacon-chain/core/helpers/beacon_committee.go b/beacon-chain/core/helpers/beacon_committee.go index 16f734bbd8..4bc21e7552 100644 --- a/beacon-chain/core/helpers/beacon_committee.go +++ b/beacon-chain/core/helpers/beacon_committee.go @@ -317,23 +317,15 @@ func ProposerAssignments(ctx context.Context, state state.BeaconState, epoch pri } proposerAssignments := make(map[primitives.ValidatorIndex][]primitives.Slot) - - originalStateSlot := state.Slot() - for slot := startSlot; slot < startSlot+params.BeaconConfig().SlotsPerEpoch; slot++ { // Skip proposer assignment for genesis slot. if slot == 0 { continue } - // Set the state's current slot. - if err := state.SetSlot(slot); err != nil { - return nil, err - } - // Determine the proposer index for the current slot. - i, err := BeaconProposerIndex(ctx, state) + i, err := BeaconProposerIndexAtSlot(ctx, state, slot) if err != nil { - return nil, errors.Wrapf(err, "could not check proposer at slot %d", state.Slot()) + return nil, errors.Wrapf(err, "could not check proposer at slot %d", slot) } // Append the slot to the proposer's assignments. @@ -342,12 +334,6 @@ func ProposerAssignments(ctx context.Context, state state.BeaconState, epoch pri } proposerAssignments[i] = append(proposerAssignments[i], slot) } - - // Reset state back to its original slot. - if err := state.SetSlot(originalStateSlot); err != nil { - return nil, err - } - return proposerAssignments, nil } diff --git a/beacon-chain/core/helpers/validators.go b/beacon-chain/core/helpers/validators.go index 46fb639db4..0675ffc1d2 100644 --- a/beacon-chain/core/helpers/validators.go +++ b/beacon-chain/core/helpers/validators.go @@ -309,23 +309,29 @@ func beaconProposerIndexAtSlotFulu(state state.ReadOnlyBeaconState, slot primiti if err != nil { return 0, errors.Wrap(err, "could not get proposer lookahead") } + spe := params.BeaconConfig().SlotsPerEpoch if e == stateEpoch { - return lookAhead[slot%params.BeaconConfig().SlotsPerEpoch], nil + return lookAhead[slot%spe], nil } // The caller is requesting the proposer for the next epoch - return lookAhead[slot%params.BeaconConfig().SlotsPerEpoch+params.BeaconConfig().SlotsPerEpoch], nil + return lookAhead[spe+slot%spe], nil } // BeaconProposerIndexAtSlot returns proposer index at the given slot from the // point of view of the given state as head state func BeaconProposerIndexAtSlot(ctx context.Context, state state.ReadOnlyBeaconState, slot primitives.Slot) (primitives.ValidatorIndex, error) { - if state.Version() >= version.Fulu { - return beaconProposerIndexAtSlotFulu(state, slot) - } e := slots.ToEpoch(slot) + stateEpoch := slots.ToEpoch(state.Slot()) + // Even if the state is post Fulu, we may request a past proposer index. + if state.Version() >= version.Fulu && e >= params.BeaconConfig().FuluForkEpoch { + // We can use the cached lookahead only for the current and the next epoch. + if e == stateEpoch || e == stateEpoch+1 { + return beaconProposerIndexAtSlotFulu(state, slot) + } + } // The cache uses the state root of the previous epoch - minimum_seed_lookahead last slot as key. (e.g. Starting epoch 1, slot 32, the key would be block root at slot 31) - // For simplicity, the node will skip caching of genesis epoch. - if e > params.BeaconConfig().GenesisEpoch+params.BeaconConfig().MinSeedLookahead { + // For simplicity, the node will skip caching of genesis epoch. If the passed state has not yet reached this slot then we do not check the cache. + if e <= stateEpoch && e > params.BeaconConfig().GenesisEpoch+params.BeaconConfig().MinSeedLookahead { s, err := slots.EpochEnd(e - 1) if err != nil { return 0, err diff --git a/beacon-chain/core/helpers/validators_test.go b/beacon-chain/core/helpers/validators_test.go index 40d5b47572..45bde7fa3e 100644 --- a/beacon-chain/core/helpers/validators_test.go +++ b/beacon-chain/core/helpers/validators_test.go @@ -1161,6 +1161,10 @@ func TestValidatorMaxEffectiveBalance(t *testing.T) { } func TestBeaconProposerIndexAtSlotFulu(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.FuluForkEpoch = 1 + params.OverrideBeaconConfig(cfg) lookahead := make([]uint64, 64) lookahead[0] = 15 lookahead[1] = 16 @@ -1180,8 +1184,4 @@ func TestBeaconProposerIndexAtSlotFulu(t *testing.T) { idx, err = helpers.BeaconProposerIndexAtSlot(t.Context(), st, 130) require.NoError(t, err) require.Equal(t, primitives.ValidatorIndex(42), idx) - _, err = helpers.BeaconProposerIndexAtSlot(t.Context(), st, 95) - require.ErrorContains(t, "slot 95 is not in the current epoch 3 or the next epoch", err) - _, err = helpers.BeaconProposerIndexAtSlot(t.Context(), st, 160) - require.ErrorContains(t, "slot 160 is not in the current epoch 3 or the next epoch", err) } diff --git a/beacon-chain/rpc/eth/validator/handlers.go b/beacon-chain/rpc/eth/validator/handlers.go index f63dbfe9d8..a44a2c3d0b 100644 --- a/beacon-chain/rpc/eth/validator/handlers.go +++ b/beacon-chain/rpc/eth/validator/handlers.go @@ -1029,8 +1029,8 @@ func (s *Server) GetProposerDuties(w http.ResponseWriter, r *http.Request) { httputil.HandleError(w, fmt.Sprintf("Could not get head state: %v ", err), http.StatusInternalServerError) return } - // Advance state with empty transitions up to the requested epoch start slot for pre fulu state only. Fulu state utilizes proposer look ahead field. - if st.Slot() < epochStartSlot && st.Version() != version.Fulu { + // Notice that even for Fulu requests for the next epoch, we are only advancing the state to the start of the current epoch. + if st.Slot() < epochStartSlot { headRoot, err := s.HeadFetcher.HeadRoot(ctx) if err != nil { httputil.HandleError(w, fmt.Sprintf("Could not get head root: %v ", err), http.StatusInternalServerError) diff --git a/beacon-chain/rpc/eth/validator/handlers_test.go b/beacon-chain/rpc/eth/validator/handlers_test.go index af69b70477..490d4ae66d 100644 --- a/beacon-chain/rpc/eth/validator/handlers_test.go +++ b/beacon-chain/rpc/eth/validator/handlers_test.go @@ -2645,78 +2645,6 @@ func TestGetProposerDuties(t *testing.T) { }) } -func TestGetProposerDuties_FuluState(t *testing.T) { - helpers.ClearCache() - - // Create a Fulu state with slot 0 (before epoch 1 start slot which is 32) - fuluState, err := util.NewBeaconStateFulu() - require.NoError(t, err) - require.NoError(t, fuluState.SetSlot(0)) // Set to slot 0 - - // Create some validators for the test - depChainStart := params.BeaconConfig().MinGenesisActiveValidatorCount - deposits, _, err := util.DeterministicDepositsAndKeys(depChainStart) - require.NoError(t, err) - - validators := make([]*ethpbalpha.Validator, len(deposits)) - for i, deposit := range deposits { - validators[i] = ðpbalpha.Validator{ - PublicKey: deposit.Data.PublicKey, - ActivationEpoch: 0, - ExitEpoch: params.BeaconConfig().FarFutureEpoch, - WithdrawalCredentials: make([]byte, 32), - } - } - require.NoError(t, fuluState.SetValidators(validators)) - - // Set up block roots - genesis := util.NewBeaconBlock() - genesisRoot, err := genesis.Block.HashTreeRoot() - require.NoError(t, err) - roots := make([][]byte, fieldparams.BlockRootsLength) - roots[0] = genesisRoot[:] - require.NoError(t, fuluState.SetBlockRoots(roots)) - - chainSlot := primitives.Slot(0) - chain := &mockChain.ChainService{ - State: fuluState, Root: genesisRoot[:], Slot: &chainSlot, - } - - db := dbutil.SetupDB(t) - require.NoError(t, db.SaveGenesisBlockRoot(t.Context(), genesisRoot)) - - s := &Server{ - Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{0: fuluState}}, - HeadFetcher: chain, - TimeFetcher: chain, - OptimisticModeFetcher: chain, - SyncChecker: &mockSync.Sync{IsSyncing: false}, - PayloadIDCache: cache.NewPayloadIDCache(), - TrackedValidatorsCache: cache.NewTrackedValidatorsCache(), - BeaconDB: db, - } - - // Request epoch 1 duties, which should require advancing from slot 0 to slot 32 - // But for Fulu state, this advancement should be skipped - request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v1/validator/duties/proposer/{epoch}", nil) - request.SetPathValue("epoch", "1") - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} - - s.GetProposerDuties(writer, request) - assert.Equal(t, http.StatusOK, writer.Code) - - // Verify the state was not advanced - it should still be at slot 0 - // This is the key assertion for the regression test - assert.Equal(t, primitives.Slot(0), fuluState.Slot(), "Fulu state should not have been advanced") - - resp := &structs.GetProposerDutiesResponse{} - require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) - - // Should still return proposer duties despite not advancing the state - assert.Equal(t, true, len(resp.Data) > 0, "Should return proposer duties even without state advancement") -} - func TestGetSyncCommitteeDuties(t *testing.T) { helpers.ClearCache() params.SetupTestConfigCleanup(t) diff --git a/changelog/potuz_next_epoch_proposer_duties.md b/changelog/potuz_next_epoch_proposer_duties.md new file mode 100644 index 0000000000..7b7e17460e --- /dev/null +++ b/changelog/potuz_next_epoch_proposer_duties.md @@ -0,0 +1,3 @@ +### Fixed + +- Fix next epoch proposer duties in Fulu by advancing the state to the beginning of the current epoch. diff --git a/testing/spectest/shared/fulu/sanity/block_processing.go b/testing/spectest/shared/fulu/sanity/block_processing.go index f5aa03f1b7..b7236c5481 100644 --- a/testing/spectest/shared/fulu/sanity/block_processing.go +++ b/testing/spectest/shared/fulu/sanity/block_processing.go @@ -8,12 +8,11 @@ import ( "strings" "testing" - "github.com/OffchainLabs/prysm/v6/config/params" - "github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/transition" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" state_native "github.com/OffchainLabs/prysm/v6/beacon-chain/state/state-native" + "github.com/OffchainLabs/prysm/v6/config/params" "github.com/OffchainLabs/prysm/v6/consensus-types/blocks" ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v6/testing/require" @@ -40,6 +39,7 @@ func RunBlockProcessingTest(t *testing.T, config, folderPath string) { params.SetupTestConfigCleanup(t) cfg := params.BeaconConfig().Copy() cfg.BlobSchedule = []params.BlobScheduleEntry{{MaxBlobsPerBlock: 9}} + cfg.FuluForkEpoch = 0 // assume epochs on tests for state are post fulu params.OverrideBeaconConfig(cfg) helpers.ClearCache()