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 all 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
29 changes: 29 additions & 0 deletions api/server/structs/endpoints_beacon.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,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 string `json:"epoch"`
PublicKeys []string `json:"public_keys,omitempty"`
Indices []string `json:"indices,omitempty"`
}

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

type IndividualVote struct {
Epoch string `json:"epoch"`
PublicKey string `json:"public_keys,omitempty"`
ValidatorIndex string `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 string `json:"current_epoch_effective_balance_gwei"`
InclusionSlot string `json:"inclusion_slot"`
InclusionDistance string `json:"inclusion_distance"`
InactivityScore string `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: fmt.Errorf("cannot retrieve information about an epoch in the future, current epoch %d, requesting %d\n", currentEpoch, req.Epoch),
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"),
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"),
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"),
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"),
Reason: Internal,
}
}
} else {
return nil, &RpcError{
Err: errors.Wrapf(err, "invalid state type retrieved with a version of %d", st.Version()),
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
20 changes: 17 additions & 3 deletions beacon-chain/rpc/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (s *Service) endpoints(
endpoints = append(endpoints, s.configEndpoints()...)
endpoints = append(endpoints, s.lightClientEndpoints(blocker, stater)...)
endpoints = append(endpoints, s.eventsEndpoints()...)
endpoints = append(endpoints, s.prysmBeaconEndpoints(ch, stater)...)
endpoints = append(endpoints, s.prysmBeaconEndpoints(ch, stater, coreService)...)
endpoints = append(endpoints, s.prysmNodeEndpoints()...)
endpoints = append(endpoints, s.prysmValidatorEndpoints(coreService)...)
if enableDebug {
Expand Down Expand Up @@ -926,8 +926,11 @@ func (s *Service) eventsEndpoints() []endpoint {
}

// Prysm custom endpoints

func (s *Service) prysmBeaconEndpoints(ch *stategen.CanonicalHistory, stater lookup.Stater) []endpoint {
func (s *Service) prysmBeaconEndpoints(
ch *stategen.CanonicalHistory,
stater lookup.Stater,
coreService *core.Service,
) []endpoint {
server := &beaconprysm.Server{
SyncChecker: s.cfg.SyncService,
HeadFetcher: s.cfg.HeadFetcher,
Expand All @@ -938,6 +941,7 @@ func (s *Service) prysmBeaconEndpoints(ch *stategen.CanonicalHistory, stater loo
Stater: stater,
ChainInfoFetcher: s.cfg.ChainInfoFetcher,
FinalizationFetcher: s.cfg.FinalizationFetcher,
CoreService: coreService,
}

const namespace = "prysm.beacon"
Expand Down Expand Up @@ -969,6 +973,16 @@ func (s *Service) prysmBeaconEndpoints(ch *stategen.CanonicalHistory, stater loo
handler: server.GetValidatorCount,
methods: []string{http.MethodGet},
},
{
template: "/prysm/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.MethodPost},
},
}
}

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},
"/prysm/v1/beacon/individual_votes": {http.MethodPost},
}

lightClientRoutes := map[string][]string{
Expand Down
17 changes: 16 additions & 1 deletion beacon-chain/rpc/prysm/beacon/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ go_library(
"//beacon-chain/blockchain:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/db:go_default_library",
"//beacon-chain/rpc/core:go_default_library",
"//beacon-chain/rpc/eth/helpers:go_default_library",
"//beacon-chain/rpc/eth/shared:go_default_library",
"//beacon-chain/rpc/lookup:go_default_library",
Expand All @@ -29,26 +30,40 @@ go_library(
"//time/slots:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_gorilla_mux//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@io_opencensus_go//trace:go_default_library",
],
)

go_test(
name = "go_default_test",
srcs = ["validator_count_test.go"],
srcs = [
"handlers_test.go",
"validator_count_test.go",
],
embed = [":go_default_library"],
deps = [
"//api/server/structs:go_default_library",
"//beacon-chain/blockchain/testing:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/db/testing:go_default_library",
"//beacon-chain/forkchoice/doubly-linked-tree:go_default_library",
"//beacon-chain/rpc/core:go_default_library",
"//beacon-chain/rpc/lookup:go_default_library",
"//beacon-chain/rpc/testutil:go_default_library",
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/stategen:go_default_library",
"//beacon-chain/state/stategen/mock:go_default_library",
"//config/params:go_default_library",
"//consensus-types/primitives:go_default_library",
"//network/httputil:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"//testing/util:go_default_library",
"//time/slots:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_gorilla_mux//:go_default_library",
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
],
)
85 changes: 85 additions & 0 deletions beacon-chain/rpc/prysm/beacon/handlers.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package beacon

import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v5/api/server/structs"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/core"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/eth/shared"
"github.com/prysmaticlabs/prysm/v5/config/params"
"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
"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 @@ -69,3 +75,82 @@ func (s *Server) GetWeakSubjectivity(w http.ResponseWriter, r *http.Request) {
}
httputil.WriteJson(w, resp)
}

// 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
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
}

publicKeyBytes := make([][]byte, len(req.PublicKeys))
for i, s := range req.PublicKeys {
bs, err := hexutil.Decode(s)
if err != nil {
httputil.HandleError(w, "could not decode public keys: "+err.Error(), http.StatusBadRequest)
return
}
publicKeyBytes[i] = bs
}
epoch, err := strconv.ParseUint(req.Epoch, 10, 64)
if err != nil {
httputil.HandleError(w, "invalid epoch: "+err.Error(), http.StatusBadRequest)
return
}
var indices []primitives.ValidatorIndex
for _, i := range req.Indices {
u, err := strconv.ParseUint(i, 10, 64)
if err != nil {
httputil.HandleError(w, "invalid indices: "+err.Error(), http.StatusBadRequest)
return
}
indices = append(indices, primitives.ValidatorIndex(u))
}
votes, rpcError := s.CoreService.IndividualVotes(
ctx,
&ethpb.IndividualVotesRequest{
Epoch: primitives.Epoch(epoch),
PublicKeys: publicKeyBytes,
Indices: 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: fmt.Sprintf("%d", vote.Epoch),
PublicKey: hexutil.Encode(vote.PublicKey),
ValidatorIndex: fmt.Sprintf("%d", 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: fmt.Sprintf("%d", vote.CurrentEpochEffectiveBalanceGwei),
InclusionSlot: fmt.Sprintf("%d", vote.InclusionSlot),
InclusionDistance: fmt.Sprintf("%d", vote.InclusionDistance),
InactivityScore: fmt.Sprintf("%d", vote.InactivityScore),
})
}
response := &structs.GetIndividualVotesResponse{
IndividualVotes: v,
}
httputil.WriteJson(w, response)
}
Loading
Loading