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 GetIndividualVotes #14198

Merged
merged 17 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions api/server/structs/endpoints_beacon.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package structs

import (
"encoding/json"

"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
)

type BlockRootResponse struct {
Expand Down Expand Up @@ -196,3 +198,32 @@ type DepositSnapshot struct {
ExecutionBlockHash string `json:"execution_block_hash"`
ExecutionBlockHeight string `json:"execution_block_height"`
}

type GetIndividualVotesRequest struct {
saolyn marked this conversation as resolved.
Show resolved Hide resolved
Epoch primitives.Epoch `json:"epoch"`
saolyn marked this conversation as resolved.
Show resolved Hide resolved
PublicKeys [][]byte `json:"public_keys,omitempty"`
Indices []primitives.ValidatorIndex `json:"indices,omitempty"`
saolyn marked this conversation as resolved.
Show resolved Hide resolved
}

type GetIndividualVotesResponse struct {
IndividualVotes []*IndividualVote `json:"individual_votes"`
}

type IndividualVote struct {
Epoch primitives.Epoch `json:"epoch"`
PublicKey []byte `json:"public_keys,omitempty"`
ValidatorIndex primitives.ValidatorIndex `json:"validator_index"`
IsSlashed bool `json:"is_slashed"`
IsWithdrawableInCurrentEpoch bool `json:"is_withdrawable_in_current_epoch"`
IsActiveInCurrentEpoch bool `json:"is_active_in_current_epoch"`
IsActiveInPreviousEpoch bool `json:"is_active_in_previous_epoch"`
IsCurrentEpochAttester bool `json:"is_current_epoch_attester"`
IsCurrentEpochTargetAttester bool `json:"is_current_epoch_target_attester"`
IsPreviousEpochAttester bool `json:"is_previous_epoch_attester"`
IsPreviousEpochTargetAttester bool `json:"is_previous_epoch_target_attester"`
IsPreviousEpochHeadAttester bool `json:"is_previous_epoch_head_attester"`
CurrentEpochEffectiveBalanceGwei uint64 `json:"current_epoch_effective_balance_gwei"`
InclusionSlot primitives.Slot `json:"inclusion_slot"`
InclusionDistance primitives.Slot `json:"inclusion_distance"`
InactivityScore uint64 `json:"inactivity_score"`
}
1 change: 1 addition & 0 deletions beacon-chain/rpc/core/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ type Service struct {
AttestationCache *cache.AttestationCache
StateGen stategen.StateManager
P2P p2p.Broadcaster
ReplayerBuilder stategen.ReplayerBuilder
OptimisticModeFetcher blockchain.OptimisticModeFetcher
}
125 changes: 125 additions & 0 deletions beacon-chain/rpc/core/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,131 @@ func (s *Service) ComputeValidatorPerformance(
}, nil
}

// IndividualVotes retrieves individual voting status of validators.
func (s *Service) IndividualVotes(
ctx context.Context,
req *ethpb.IndividualVotesRequest,
) (*ethpb.IndividualVotesRespond, *RpcError) {
currentEpoch := slots.ToEpoch(s.GenesisTimeFetcher.CurrentSlot())
if req.Epoch > currentEpoch {
return nil, &RpcError{
Err: errors.New(fmt.Sprintf("Cannot retrieve information about an epoch in the future, current epoch %d, requesting %d\n", currentEpoch, req.Epoch)),
saolyn marked this conversation as resolved.
Show resolved Hide resolved
Reason: BadRequest,
}
}

slot, err := slots.EpochEnd(req.Epoch)
if err != nil {
return nil, &RpcError{Err: err, Reason: Internal}
}
st, err := s.ReplayerBuilder.ReplayerForSlot(slot).ReplayBlocks(ctx)
if err != nil {
return nil, &RpcError{
Err: errors.Wrapf(err, "failed to replay blocks for state at epoch %d", req.Epoch),
Reason: Internal,
}
}
// Track filtered validators to prevent duplication in the response.
filtered := map[primitives.ValidatorIndex]bool{}
filteredIndices := make([]primitives.ValidatorIndex, 0)
votes := make([]*ethpb.IndividualVotesRespond_IndividualVote, 0, len(req.Indices)+len(req.PublicKeys))
// Filter out assignments by public keys.
for _, pubKey := range req.PublicKeys {
index, ok := st.ValidatorIndexByPubkey(bytesutil.ToBytes48(pubKey))
if !ok {
votes = append(votes, &ethpb.IndividualVotesRespond_IndividualVote{PublicKey: pubKey, ValidatorIndex: primitives.ValidatorIndex(^uint64(0))})
continue
}
filtered[index] = true
filteredIndices = append(filteredIndices, index)
}
// Filter out assignments by validator indices.
for _, index := range req.Indices {
if !filtered[index] {
filteredIndices = append(filteredIndices, index)
}
}
sort.Slice(filteredIndices, func(i, j int) bool {
return filteredIndices[i] < filteredIndices[j]
})

var v []*precompute.Validator
var bal *precompute.Balance
if st.Version() == version.Phase0 {
v, bal, err = precompute.New(ctx, st)
if err != nil {
return nil, &RpcError{
Err: errors.Wrapf(err, "Could not set up pre compute instance"),
saolyn marked this conversation as resolved.
Show resolved Hide resolved
Reason: Internal,
}
}
v, _, err = precompute.ProcessAttestations(ctx, st, v, bal)
if err != nil {
return nil, &RpcError{
Err: errors.Wrapf(err, "Could not pre compute attestations"),
saolyn marked this conversation as resolved.
Show resolved Hide resolved
Reason: Internal,
}
}
} else if st.Version() >= version.Altair {
v, bal, err = altair.InitializePrecomputeValidators(ctx, st)
if err != nil {
return nil, &RpcError{
Err: errors.Wrapf(err, "Could not set up altair pre compute instance"),
saolyn marked this conversation as resolved.
Show resolved Hide resolved
Reason: Internal,
}
}
v, _, err = altair.ProcessEpochParticipation(ctx, st, bal, v)
if err != nil {
return nil, &RpcError{
Err: errors.Wrapf(err, "Could not pre compute attestations"),
saolyn marked this conversation as resolved.
Show resolved Hide resolved
Reason: Internal,
}
}
} else {
return nil, &RpcError{
Err: errors.Wrapf(err, "Invalid state type retrieved with a version of %d", st.Version()),
saolyn marked this conversation as resolved.
Show resolved Hide resolved
Reason: Internal,
}
}

for _, index := range filteredIndices {
if uint64(index) >= uint64(len(v)) {
votes = append(votes, &ethpb.IndividualVotesRespond_IndividualVote{ValidatorIndex: index})
continue
}
val, err := st.ValidatorAtIndexReadOnly(index)
if err != nil {
return nil, &RpcError{
Err: errors.Wrapf(err, "Could not retrieve validator"),
Reason: Internal,
}
}
pb := val.PublicKey()
votes = append(votes, &ethpb.IndividualVotesRespond_IndividualVote{
Epoch: req.Epoch,
PublicKey: pb[:],
ValidatorIndex: index,
IsSlashed: v[index].IsSlashed,
IsWithdrawableInCurrentEpoch: v[index].IsWithdrawableCurrentEpoch,
IsActiveInCurrentEpoch: v[index].IsActiveCurrentEpoch,
IsActiveInPreviousEpoch: v[index].IsActivePrevEpoch,
IsCurrentEpochAttester: v[index].IsCurrentEpochAttester,
IsCurrentEpochTargetAttester: v[index].IsCurrentEpochTargetAttester,
IsPreviousEpochAttester: v[index].IsPrevEpochAttester,
IsPreviousEpochTargetAttester: v[index].IsPrevEpochTargetAttester,
IsPreviousEpochHeadAttester: v[index].IsPrevEpochHeadAttester,
CurrentEpochEffectiveBalanceGwei: v[index].CurrentEpochEffectiveBalance,
InclusionSlot: v[index].InclusionSlot,
InclusionDistance: v[index].InclusionDistance,
InactivityScore: v[index].InactivityScore,
})
}

return &ethpb.IndividualVotesRespond{
IndividualVotes: votes,
}, nil
}

// SubmitSignedContributionAndProof is called by a sync committee aggregator
// to submit signed contribution and proof object.
func (s *Service) SubmitSignedContributionAndProof(
Expand Down
10 changes: 10 additions & 0 deletions beacon-chain/rpc/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,16 @@ func (s *Service) beaconEndpoints(
handler: server.GetValidatorBalances,
methods: []string{http.MethodGet, http.MethodPost},
},
{
template: "/eth/v1/beacon/individual_votes",
name: namespace + ".GetIndividualVotes",
middleware: []mux.MiddlewareFunc{
middleware.ContentTypeHandler([]string{api.JsonMediaType}),
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
},
handler: server.GetIndividualVotes,
methods: []string{http.MethodGet},
},
saolyn marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
1 change: 1 addition & 0 deletions beacon-chain/rpc/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func Test_endpoints(t *testing.T) {
"/eth/v1/beacon/pool/sync_committees": {http.MethodPost},
"/eth/v1/beacon/pool/voluntary_exits": {http.MethodGet, http.MethodPost},
"/eth/v1/beacon/pool/bls_to_execution_changes": {http.MethodGet, http.MethodPost},
"/eth/v1/beacon/individual_votes": {http.MethodGet},
}

lightClientRoutes := map[string][]string{
Expand Down
3 changes: 3 additions & 0 deletions beacon-chain/rpc/eth/beacon/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ go_test(
"//api/server/structs:go_default_library",
"//beacon-chain/blockchain/testing:go_default_library",
"//beacon-chain/cache/depositsnapshot:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/signing:go_default_library",
"//beacon-chain/core/time:go_default_library",
"//beacon-chain/core/transition:go_default_library",
Expand All @@ -100,6 +101,8 @@ go_test(
"//beacon-chain/rpc/testutil:go_default_library",
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/state-native:go_default_library",
"//beacon-chain/state/stategen:go_default_library",
"//beacon-chain/state/stategen/mock:go_default_library",
"//beacon-chain/sync/initial-sync/testing:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
Expand Down
57 changes: 57 additions & 0 deletions beacon-chain/rpc/eth/beacon/handlers_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v5/api/server/structs"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/core"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/eth/helpers"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/eth/shared"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/state"
Expand All @@ -21,6 +22,7 @@ import (
"github.com/prysmaticlabs/prysm/v5/consensus-types/validator"
"github.com/prysmaticlabs/prysm/v5/encoding/bytesutil"
"github.com/prysmaticlabs/prysm/v5/network/httputil"
ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v5/time/slots"
"go.opencensus.io/trace"
)
Expand Down Expand Up @@ -415,3 +417,58 @@ func valContainerFromReadOnlyVal(
},
}
}

// GetIndividualVotes returns a list of validators individual vote status of a given epoch.
func (s *Server) GetIndividualVotes(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.GetIndividualVotes")
defer span.End()

var req structs.GetIndividualVotesRequest
if r.Body != http.NoBody {
Copy link
Contributor

@rkapka rkapka Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. You made the endpoint a GET, which doesn't have a body in the request. We can either put the epoch, public keys and indices in query params or make the endpoint a POST. Given that the length of a query string can be limited by some servers, the latter is a much better option.
  2. What if there is no body? The function will panic when reading req.Epoch. I see we have the same pattern in GetValidatorPerformance, but this is a bug. We should return a BadRequest error in this case. What we usually do:
err = json.NewDecoder(r.Body).Decode(&req)
		switch {
		case errors.Is(err, io.EOF):
			httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
			return
		case err != nil:
			httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
			return
		}

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httputil.HandleError(
w,
fmt.Sprintf("Could not decode request body: %v", err.Error()),
http.StatusBadRequest,
)
return
}
}
votes, rpcError := s.CoreService.IndividualVotes(
ctx,
&ethpb.IndividualVotesRequest{
Epoch: req.Epoch,
PublicKeys: req.PublicKeys,
Indices: req.Indices,
},
)
if rpcError != nil {
httputil.HandleError(w, rpcError.Err.Error(), core.ErrorReasonToHTTP(rpcError.Reason))
return
}
v := make([]*structs.IndividualVote, 0, len(votes.IndividualVotes))
for _, vote := range votes.IndividualVotes {
v = append(v, &structs.IndividualVote{
Epoch: vote.Epoch,
PublicKey: vote.PublicKey,
ValidatorIndex: vote.ValidatorIndex,
IsSlashed: vote.IsSlashed,
IsWithdrawableInCurrentEpoch: vote.IsWithdrawableInCurrentEpoch,
IsActiveInCurrentEpoch: vote.IsActiveInCurrentEpoch,
IsActiveInPreviousEpoch: vote.IsActiveInPreviousEpoch,
IsCurrentEpochAttester: vote.IsCurrentEpochAttester,
IsCurrentEpochTargetAttester: vote.IsCurrentEpochTargetAttester,
IsPreviousEpochAttester: vote.IsPreviousEpochAttester,
IsPreviousEpochTargetAttester: vote.IsPreviousEpochTargetAttester,
IsPreviousEpochHeadAttester: vote.IsPreviousEpochHeadAttester,
CurrentEpochEffectiveBalanceGwei: vote.CurrentEpochEffectiveBalanceGwei,
InclusionSlot: vote.InclusionSlot,
InclusionDistance: vote.InclusionDistance,
InactivityScore: vote.InactivityScore,
})
}
response := &structs.GetIndividualVotesResponse{
IndividualVotes: v,
}
httputil.WriteJson(w, response)
}
Loading
Loading