Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP endpoint for GetValidatorParticipation #14261

Merged
merged 15 commits into from
Aug 2, 2024
19 changes: 19 additions & 0 deletions api/server/structs/endpoints_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,22 @@ type GetValidatorPerformanceResponse struct {
MissingValidators [][]byte `json:"missing_validators,omitempty"`
InactivityScores []uint64 `json:"inactivity_scores,omitempty"`
}

type GetValidatorParticipationResponse struct {
Epoch string `json:"epoch"`
Finalized bool `json:"finalized"`
Participation *ValidatorParticipation `json:"participation"`
}

type ValidatorParticipation struct {
GlobalParticipationRate string `json:"global_participation_rate" deprecated:"true"`
VotedEther string `json:"voted_ether" deprecated:"true"`
EligibleEther string `json:"eligible_ether" deprecated:"true"`
CurrentEpochActiveGwei string `json:"current_epoch_active_gwei"`
CurrentEpochAttestingGwei string `json:"current_epoch_attesting_gwei"`
CurrentEpochTargetAttestingGwei string `json:"current_epoch_target_attesting_gwei"`
PreviousEpochActiveGwei string `json:"previous_epoch_active_gwei"`
PreviousEpochAttestingGwei string `json:"previous_epoch_attesting_gwei"`
PreviousEpochTargetAttestingGwei string `json:"previous_epoch_target_attesting_gwei"`
PreviousEpochHeadAttestingGwei string `json:"previous_epoch_head_attesting_gwei"`
}
84 changes: 84 additions & 0 deletions beacon-chain/rpc/core/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -752,3 +752,87 @@ func subnetsFromCommittee(pubkey []byte, comm *ethpb.SyncCommittee) []uint64 {
}
return positions
}

// ValidatorParticipation retrieves the validator participation information for a given epoch,
// it returns the information about validator's participation rate in voting on the proof of stake
// rules based on their balance compared to the total active validator balance.
func (s *Service) ValidatorParticipation(
ctx context.Context,
requestedEpoch primitives.Epoch,
) (
*ethpb.ValidatorParticipationResponse,
*RpcError,
) {
currentSlot := s.GenesisTimeFetcher.CurrentSlot()
currentEpoch := slots.ToEpoch(currentSlot)

if requestedEpoch > currentEpoch {
return nil, &RpcError{
Err: fmt.Errorf("cannot retrieve information about an epoch greater than current epoch, current epoch %d, requesting %d", currentEpoch, requestedEpoch),
Reason: BadRequest,
}
}
// Use the last slot of requested epoch to obtain current and previous epoch attestations.
// This ensures that we don't miss previous attestations when input requested epochs.
endSlot, err := slots.EpochEnd(requestedEpoch)
if err != nil {
return nil, &RpcError{Reason: Internal, Err: errors.Wrap(err, "could not get slot from requested epoch")}
}
// Get as close as we can to the end of the current epoch without going past the current slot.
// The above check ensures a future *epoch* isn't requested, but the end slot of the requested epoch could still
// be past the current slot. In that case, use the current slot as the best approximation of the requested epoch.
// Replayer will make sure the slot ultimately used is canonical.
if endSlot > currentSlot {
endSlot = currentSlot
}

// ReplayerBuilder ensures that a canonical chain is followed to the slot
beaconSt, err := s.ReplayerBuilder.ReplayerForSlot(endSlot).ReplayBlocks(ctx)
if err != nil {
return nil, &RpcError{Reason: Internal, Err: errors.Wrapf(err, "error replaying blocks for state at slot %d", endSlot)}
}
var v []*precompute.Validator
var b *precompute.Balance

if beaconSt.Version() == version.Phase0 {
v, b, err = precompute.New(ctx, beaconSt)
if err != nil {
return nil, &RpcError{Reason: Internal, Err: errors.Wrap(err, "could not set up pre compute instance")}
}
_, b, err = precompute.ProcessAttestations(ctx, beaconSt, v, b)
if err != nil {
return nil, &RpcError{Reason: Internal, Err: errors.Wrap(err, "could not pre compute attestations")}
}
} else if beaconSt.Version() >= version.Altair {
v, b, err = altair.InitializePrecomputeValidators(ctx, beaconSt)
if err != nil {
return nil, &RpcError{Reason: Internal, Err: errors.Wrap(err, "could not set up altair pre compute instance")}
}
_, b, err = altair.ProcessEpochParticipation(ctx, beaconSt, b, v)
if err != nil {
return nil, &RpcError{Reason: Internal, Err: errors.Wrap(err, "could not pre compute attestations: %v")}
}
} else {
return nil, &RpcError{Reason: Internal, Err: fmt.Errorf("invalid state type retrieved with a version of %s", version.String(beaconSt.Version()))}
}

cp := s.FinalizedFetcher.FinalizedCheckpt()
p := &ethpb.ValidatorParticipationResponse{
Epoch: requestedEpoch,
Finalized: requestedEpoch <= cp.Epoch,
Participation: &ethpb.ValidatorParticipation{
// TODO(7130): Remove these three deprecated fields.
GlobalParticipationRate: float32(b.PrevEpochTargetAttested) / float32(b.ActivePrevEpoch),
VotedEther: b.PrevEpochTargetAttested,
EligibleEther: b.ActivePrevEpoch,
CurrentEpochActiveGwei: b.ActiveCurrentEpoch,
CurrentEpochAttestingGwei: b.CurrentEpochAttested,
CurrentEpochTargetAttestingGwei: b.CurrentEpochTargetAttested,
PreviousEpochActiveGwei: b.ActivePrevEpoch,
PreviousEpochAttestingGwei: b.PrevEpochAttested,
PreviousEpochTargetAttestingGwei: b.PrevEpochTargetAttested,
PreviousEpochHeadAttestingGwei: b.PrevEpochHeadAttested,
},
}
return p, nil
}
17 changes: 14 additions & 3 deletions beacon-chain/rpc/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (s *Service) endpoints(
endpoints = append(endpoints, s.eventsEndpoints()...)
endpoints = append(endpoints, s.prysmBeaconEndpoints(ch, stater, coreService)...)
endpoints = append(endpoints, s.prysmNodeEndpoints()...)
endpoints = append(endpoints, s.prysmValidatorEndpoints(coreService)...)
endpoints = append(endpoints, s.prysmValidatorEndpoints(stater, coreService)...)
if enableDebug {
endpoints = append(endpoints, s.debugEndpoints(stater)...)
}
Expand Down Expand Up @@ -1060,9 +1060,11 @@ func (s *Service) prysmNodeEndpoints() []endpoint {
}
}

func (*Service) prysmValidatorEndpoints(coreService *core.Service) []endpoint {
func (s *Service) prysmValidatorEndpoints(stater lookup.Stater, coreService *core.Service) []endpoint {
server := &validatorprysm.Server{
CoreService: coreService,
ChainInfoFetcher: s.cfg.ChainInfoFetcher,
Stater: stater,
CoreService: coreService,
}

const namespace = "prysm.validator"
Expand All @@ -1087,5 +1089,14 @@ func (*Service) prysmValidatorEndpoints(coreService *core.Service) []endpoint {
handler: server.GetValidatorPerformance,
methods: []string{http.MethodPost},
},
{
template: "/prysm/v1/validators/participation",
name: namespace + ".GetValidatorParticipation",
middleware: []mux.MiddlewareFunc{
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
},
handler: server.GetValidatorParticipation,
methods: []string{http.MethodGet},
},
}
}
5 changes: 3 additions & 2 deletions beacon-chain/rpc/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ func Test_endpoints(t *testing.T) {
}

prysmValidatorRoutes := map[string][]string{
"/prysm/validators/performance": {http.MethodPost},
"/prysm/v1/validators/performance": {http.MethodPost},
"/prysm/validators/performance": {http.MethodPost},
"/prysm/v1/validators/performance": {http.MethodPost},
"/prysm/v1/validators/participation": {http.MethodGet},
}

s := &Service{cfg: &Config{}}
Expand Down
2 changes: 0 additions & 2 deletions beacon-chain/rpc/prysm/v1alpha1/beacon/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ go_library(
"//api/pagination:go_default_library",
"//beacon-chain/blockchain:go_default_library",
"//beacon-chain/cache:go_default_library",
"//beacon-chain/core/altair:go_default_library",
"//beacon-chain/core/epoch/precompute:go_default_library",
"//beacon-chain/core/feed/block:go_default_library",
"//beacon-chain/core/feed/operation:go_default_library",
"//beacon-chain/core/feed/state:go_default_library",
Expand Down
78 changes: 4 additions & 74 deletions beacon-chain/rpc/prysm/v1alpha1/beacon/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"strconv"

"github.com/prysmaticlabs/prysm/v5/api/pagination"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/altair"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/epoch/precompute"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers"
coreTime "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/time"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/transition"
Expand Down Expand Up @@ -477,7 +475,7 @@ func (bs *Server) GetValidatorActiveSetChanges(
func (bs *Server) GetValidatorParticipation(
ctx context.Context, req *ethpb.GetValidatorParticipationRequest,
) (*ethpb.ValidatorParticipationResponse, error) {
currentSlot := bs.GenesisTimeFetcher.CurrentSlot()
currentSlot := bs.CoreService.GenesisTimeFetcher.CurrentSlot()
currentEpoch := slots.ToEpoch(currentSlot)

var requestedEpoch primitives.Epoch
Expand All @@ -489,79 +487,11 @@ func (bs *Server) GetValidatorParticipation(
default:
requestedEpoch = currentEpoch
}

if requestedEpoch > currentEpoch {
return nil, status.Errorf(
codes.InvalidArgument,
"Cannot retrieve information about an epoch greater than current epoch, current epoch %d, requesting %d",
currentEpoch,
requestedEpoch,
)
}
// Use the last slot of requested epoch to obtain current and previous epoch attestations.
// This ensures that we don't miss previous attestations when input requested epochs.
endSlot, err := slots.EpochEnd(requestedEpoch)
vp, err := bs.CoreService.ValidatorParticipation(ctx, requestedEpoch)
if err != nil {
return nil, err
return nil, status.Errorf(core.ErrorReasonToGRPC(err.Reason), "Could not retrieve validator participation: %v", err.Err)
}
// Get as close as we can to the end of the current epoch without going past the current slot.
// The above check ensures a future *epoch* isn't requested, but the end slot of the requested epoch could still
// be past the current slot. In that case, use the current slot as the best approximation of the requested epoch.
// Replayer will make sure the slot ultimately used is canonical.
if endSlot > currentSlot {
endSlot = currentSlot
}

// ReplayerBuilder ensures that a canonical chain is followed to the slot
beaconState, err := bs.ReplayerBuilder.ReplayerForSlot(endSlot).ReplayBlocks(ctx)
if err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("error replaying blocks for state at slot %d: %v", endSlot, err))
}
var v []*precompute.Validator
var b *precompute.Balance

if beaconState.Version() == version.Phase0 {
v, b, err = precompute.New(ctx, beaconState)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not set up pre compute instance: %v", err)
}
_, b, err = precompute.ProcessAttestations(ctx, beaconState, v, b)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not pre compute attestations: %v", err)
}
} else if beaconState.Version() >= version.Altair {
v, b, err = altair.InitializePrecomputeValidators(ctx, beaconState)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not set up altair pre compute instance: %v", err)
}
_, b, err = altair.ProcessEpochParticipation(ctx, beaconState, b, v)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not pre compute attestations: %v", err)
}
} else {
return nil, status.Errorf(codes.Internal, "Invalid state type retrieved with a version of %d", beaconState.Version())
}

cp := bs.FinalizationFetcher.FinalizedCheckpt()
p := &ethpb.ValidatorParticipationResponse{
Epoch: requestedEpoch,
Finalized: requestedEpoch <= cp.Epoch,
Participation: &ethpb.ValidatorParticipation{
// TODO(7130): Remove these three deprecated fields.
GlobalParticipationRate: float32(b.PrevEpochTargetAttested) / float32(b.ActivePrevEpoch),
VotedEther: b.PrevEpochTargetAttested,
EligibleEther: b.ActivePrevEpoch,
CurrentEpochActiveGwei: b.ActiveCurrentEpoch,
CurrentEpochAttestingGwei: b.CurrentEpochAttested,
CurrentEpochTargetAttestingGwei: b.CurrentEpochTargetAttested,
PreviousEpochActiveGwei: b.ActivePrevEpoch,
PreviousEpochAttestingGwei: b.PrevEpochAttested,
PreviousEpochTargetAttestingGwei: b.PrevEpochTargetAttested,
PreviousEpochHeadAttestingGwei: b.PrevEpochHeadAttested,
},
}

return p, nil
return vp, nil
}

// GetValidatorQueue retrieves the current validator queue information.
Expand Down
56 changes: 30 additions & 26 deletions beacon-chain/rpc/prysm/v1alpha1/beacon/validators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1476,27 +1476,25 @@ func TestServer_GetValidatorQueue_PendingExit(t *testing.T) {
}

func TestServer_GetValidatorParticipation_CannotRequestFutureEpoch(t *testing.T) {
beaconDB := dbTest.SetupDB(t)

ctx := context.Background()
headState, err := util.NewBeaconState()
require.NoError(t, err)
require.NoError(t, headState.SetSlot(0))
bs := &Server{
BeaconDB: beaconDB,
HeadFetcher: &mock.ChainService{
State: headState,
CoreService: &core.Service{
HeadFetcher: &mock.ChainService{
State: headState,
},
GenesisTimeFetcher: &mock.ChainService{},
},
GenesisTimeFetcher: &mock.ChainService{},
StateGen: stategen.New(beaconDB, doublylinkedtree.New()),
}

wanted := "Cannot retrieve information about an epoch"
wanted := "cannot retrieve information about an epoch"
_, err = bs.GetValidatorParticipation(
ctx,
&ethpb.GetValidatorParticipationRequest{
QueryFilter: &ethpb.GetValidatorParticipationRequest_Epoch{
Epoch: slots.ToEpoch(bs.GenesisTimeFetcher.CurrentSlot()) + 1,
Epoch: slots.ToEpoch(bs.CoreService.GenesisTimeFetcher.CurrentSlot()) + 1,
},
},
)
Expand Down Expand Up @@ -1549,18 +1547,20 @@ func TestServer_GetValidatorParticipation_CurrentAndPrevEpoch(t *testing.T) {
m := &mock.ChainService{State: headState}
offset := int64(params.BeaconConfig().SlotsPerEpoch.Mul(params.BeaconConfig().SecondsPerSlot))
bs := &Server{
BeaconDB: beaconDB,
HeadFetcher: m,
StateGen: stategen.New(beaconDB, doublylinkedtree.New()),
GenesisTimeFetcher: &mock.ChainService{
Genesis: prysmTime.Now().Add(time.Duration(-1*offset) * time.Second),
BeaconDB: beaconDB,
StateGen: stategen.New(beaconDB, doublylinkedtree.New()),
CoreService: &core.Service{
HeadFetcher: m,
GenesisTimeFetcher: &mock.ChainService{
Genesis: prysmTime.Now().Add(time.Duration(-1*offset) * time.Second),
},
FinalizedFetcher: &mock.ChainService{FinalizedCheckPoint: &ethpb.Checkpoint{Epoch: 100}},
},
CanonicalFetcher: &mock.ChainService{
CanonicalRoots: map[[32]byte]bool{
bRoot: true,
},
},
FinalizationFetcher: &mock.ChainService{FinalizedCheckPoint: &ethpb.Checkpoint{Epoch: 100}},
}
addDefaultReplayerBuilder(bs, beaconDB)

Expand Down Expand Up @@ -1628,18 +1628,20 @@ func TestServer_GetValidatorParticipation_OrphanedUntilGenesis(t *testing.T) {
m := &mock.ChainService{State: headState}
offset := int64(params.BeaconConfig().SlotsPerEpoch.Mul(params.BeaconConfig().SecondsPerSlot))
bs := &Server{
BeaconDB: beaconDB,
HeadFetcher: m,
StateGen: stategen.New(beaconDB, doublylinkedtree.New()),
GenesisTimeFetcher: &mock.ChainService{
Genesis: prysmTime.Now().Add(time.Duration(-1*offset) * time.Second),
BeaconDB: beaconDB,
StateGen: stategen.New(beaconDB, doublylinkedtree.New()),
CoreService: &core.Service{
HeadFetcher: m,
GenesisTimeFetcher: &mock.ChainService{
Genesis: prysmTime.Now().Add(time.Duration(-1*offset) * time.Second),
},
FinalizedFetcher: &mock.ChainService{FinalizedCheckPoint: &ethpb.Checkpoint{Epoch: 100}},
},
CanonicalFetcher: &mock.ChainService{
CanonicalRoots: map[[32]byte]bool{
bRoot: true,
},
},
FinalizationFetcher: &mock.ChainService{FinalizedCheckPoint: &ethpb.Checkpoint{Epoch: 100}},
}
addDefaultReplayerBuilder(bs, beaconDB)

Expand Down Expand Up @@ -1744,13 +1746,15 @@ func runGetValidatorParticipationCurrentAndPrevEpoch(t *testing.T, genState stat
m := &mock.ChainService{State: genState}
offset := int64(params.BeaconConfig().SlotsPerEpoch.Mul(params.BeaconConfig().SecondsPerSlot))
bs := &Server{
BeaconDB: beaconDB,
BeaconDB: beaconDB,
CoreService: &core.Service{
GenesisTimeFetcher: &mock.ChainService{
Genesis: prysmTime.Now().Add(time.Duration(-1*offset) * time.Second),
},
FinalizedFetcher: &mock.ChainService{FinalizedCheckPoint: &ethpb.Checkpoint{Epoch: 100}},
},
HeadFetcher: m,
StateGen: stategen.New(beaconDB, doublylinkedtree.New()),
GenesisTimeFetcher: &mock.ChainService{
Genesis: prysmTime.Now().Add(time.Duration(-1*offset) * time.Second),
},
FinalizationFetcher: &mock.ChainService{FinalizedCheckPoint: &ethpb.Checkpoint{Epoch: 100}},
}
addDefaultReplayerBuilder(bs, beaconDB)

Expand Down
Loading
Loading