package rewards import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "github.com/prysmaticlabs/prysm/v4/beacon-chain/core/altair" "github.com/prysmaticlabs/prysm/v4/beacon-chain/core/epoch/precompute" "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/shared" "github.com/prysmaticlabs/prysm/v4/beacon-chain/state" fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" "github.com/prysmaticlabs/prysm/v4/config/params" "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" http2 "github.com/prysmaticlabs/prysm/v4/network/http" "github.com/prysmaticlabs/prysm/v4/runtime/version" "github.com/prysmaticlabs/prysm/v4/time/slots" "github.com/wealdtech/go-bytesutil" "go.opencensus.io/trace" ) // BlockRewards is an HTTP handler for Beacon API getBlockRewards. func (s *Server) BlockRewards(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "beacon.BlockRewards") defer span.End() segments := strings.Split(r.URL.Path, "/") blockId := segments[len(segments)-1] blk, err := s.Blocker.Block(r.Context(), []byte(blockId)) if !shared.WriteBlockFetchError(w, blk, err) { return } if blk.Version() == version.Phase0 { http2.HandleError(w, "Block rewards are not supported for Phase 0 blocks", http.StatusBadRequest) return } optimistic, err := s.OptimisticModeFetcher.IsOptimistic(r.Context()) if err != nil { http2.HandleError(w, "Could not get optimistic mode info: "+err.Error(), http.StatusInternalServerError) return } blkRoot, err := blk.Block().HashTreeRoot() if err != nil { http2.HandleError(w, "Could not get block root: "+err.Error(), http.StatusInternalServerError) return } blockRewards, httpError := s.BlockRewardFetcher.GetBlockRewardsData(ctx, blk) if httpError != nil { http2.WriteError(w, httpError) return } response := &BlockRewardsResponse{ Data: blockRewards, ExecutionOptimistic: optimistic, Finalized: s.FinalizationFetcher.IsFinalized(ctx, blkRoot), } http2.WriteJson(w, response) } // AttestationRewards retrieves attestation reward info for validators specified by array of public keys or validator index. // If no array is provided, return reward info for every validator. // TODO: Inclusion delay func (s *Server) AttestationRewards(w http.ResponseWriter, r *http.Request) { st, ok := s.attRewardsState(w, r) if !ok { return } bal, vals, valIndices, ok := attRewardsBalancesAndVals(w, r, st) if !ok { return } totalRewards, ok := totalAttRewards(w, st, bal, vals, valIndices) if !ok { return } idealRewards, ok := idealAttRewards(w, st, bal, vals) if !ok { return } optimistic, err := s.OptimisticModeFetcher.IsOptimistic(r.Context()) if err != nil { http2.HandleError(w, "Could not get optimistic mode info: "+err.Error(), http.StatusInternalServerError) return } blkRoot, err := st.LatestBlockHeader().HashTreeRoot() if err != nil { http2.HandleError(w, "Could not get block root: "+err.Error(), http.StatusInternalServerError) return } resp := &AttestationRewardsResponse{ Data: AttestationRewards{ IdealRewards: idealRewards, TotalRewards: totalRewards, }, ExecutionOptimistic: optimistic, Finalized: s.FinalizationFetcher.IsFinalized(r.Context(), blkRoot), } http2.WriteJson(w, resp) } // SyncCommitteeRewards retrieves rewards info for sync committee members specified by array of public keys or validator index. // If no array is provided, return reward info for every committee member. func (s *Server) SyncCommitteeRewards(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "beacon.SyncCommitteeRewards") defer span.End() segments := strings.Split(r.URL.Path, "/") blockId := segments[len(segments)-1] blk, err := s.Blocker.Block(r.Context(), []byte(blockId)) if !shared.WriteBlockFetchError(w, blk, err) { return } if blk.Version() == version.Phase0 { http2.HandleError(w, "Sync committee rewards are not supported for Phase 0", http.StatusBadRequest) return } st, httpErr := s.BlockRewardFetcher.GetStateForRewards(ctx, blk) if httpErr != nil { http2.WriteError(w, httpErr) return } sa, err := blk.Block().Body().SyncAggregate() if err != nil { http2.HandleError(w, "Could not get sync aggregate: "+err.Error(), http.StatusInternalServerError) return } vals, valIndices, ok := syncRewardsVals(w, r, st) if !ok { return } preProcessBals := make([]uint64, len(vals)) for i, valIdx := range valIndices { preProcessBals[i], err = st.BalanceAtIndex(valIdx) if err != nil { http2.HandleError(w, "Could not get validator's balance: "+err.Error(), http.StatusInternalServerError) return } } _, proposerReward, err := altair.ProcessSyncAggregate(r.Context(), st, sa) if err != nil { http2.HandleError(w, "Could not get sync aggregate rewards: "+err.Error(), http.StatusInternalServerError) return } rewards := make([]int, len(preProcessBals)) proposerIndex := blk.Block().ProposerIndex() for i, valIdx := range valIndices { bal, err := st.BalanceAtIndex(valIdx) if err != nil { http2.HandleError(w, "Could not get validator's balance: "+err.Error(), http.StatusInternalServerError) return } rewards[i] = int(bal - preProcessBals[i]) // lint:ignore uintcast if valIdx == proposerIndex { rewards[i] = rewards[i] - int(proposerReward) // lint:ignore uintcast } } optimistic, err := s.OptimisticModeFetcher.IsOptimistic(r.Context()) if err != nil { http2.HandleError(w, "Could not get optimistic mode info: "+err.Error(), http.StatusInternalServerError) return } blkRoot, err := blk.Block().HashTreeRoot() if err != nil { http2.HandleError(w, "Could not get block root: "+err.Error(), http.StatusInternalServerError) return } scRewards := make([]SyncCommitteeReward, len(valIndices)) for i, valIdx := range valIndices { scRewards[i] = SyncCommitteeReward{ ValidatorIndex: strconv.FormatUint(uint64(valIdx), 10), Reward: strconv.Itoa(rewards[i]), } } response := &SyncCommitteeRewardsResponse{ Data: scRewards, ExecutionOptimistic: optimistic, Finalized: s.FinalizationFetcher.IsFinalized(r.Context(), blkRoot), } http2.WriteJson(w, response) } func (s *Server) attRewardsState(w http.ResponseWriter, r *http.Request) (state.BeaconState, bool) { segments := strings.Split(r.URL.Path, "/") requestedEpoch, err := strconv.ParseUint(segments[len(segments)-1], 10, 64) if err != nil { http2.HandleError(w, "Could not decode epoch: "+err.Error(), http.StatusBadRequest) return nil, false } if primitives.Epoch(requestedEpoch) < params.BeaconConfig().AltairForkEpoch { http2.HandleError(w, "Attestation rewards are not supported for Phase 0", http.StatusNotFound) return nil, false } currentEpoch := uint64(slots.ToEpoch(s.TimeFetcher.CurrentSlot())) if requestedEpoch+1 >= currentEpoch { http2.HandleError(w, "Attestation rewards are available after two epoch transitions to ensure all attestations have a chance of inclusion", http.StatusNotFound) return nil, false } nextEpochEnd, err := slots.EpochEnd(primitives.Epoch(requestedEpoch + 1)) if err != nil { http2.HandleError(w, "Could not get next epoch's ending slot: "+err.Error(), http.StatusInternalServerError) return nil, false } st, err := s.Stater.StateBySlot(r.Context(), nextEpochEnd) if err != nil { http2.HandleError(w, "Could not get state for epoch's starting slot: "+err.Error(), http.StatusInternalServerError) return nil, false } return st, true } func attRewardsBalancesAndVals( w http.ResponseWriter, r *http.Request, st state.BeaconState, ) (*precompute.Balance, []*precompute.Validator, []primitives.ValidatorIndex, bool) { allVals, bal, err := altair.InitializePrecomputeValidators(r.Context(), st) if err != nil { http2.HandleError(w, "Could not initialize precompute validators: "+err.Error(), http.StatusBadRequest) return nil, nil, nil, false } allVals, bal, err = altair.ProcessEpochParticipation(r.Context(), st, bal, allVals) if err != nil { http2.HandleError(w, "Could not process epoch participation: "+err.Error(), http.StatusBadRequest) return nil, nil, nil, false } valIndices, ok := requestedValIndices(w, r, st, allVals) if !ok { return nil, nil, nil, false } if len(valIndices) == len(allVals) { return bal, allVals, valIndices, true } else { filteredVals := make([]*precompute.Validator, len(valIndices)) for i, valIx := range valIndices { filteredVals[i] = allVals[valIx] } return bal, filteredVals, valIndices, true } } // idealAttRewards returns rewards for hypothetical, perfectly voting validators // whose effective balances are over EJECTION_BALANCE and match balances in passed in validators. func idealAttRewards( w http.ResponseWriter, st state.BeaconState, bal *precompute.Balance, vals []*precompute.Validator, ) ([]IdealAttestationReward, bool) { idealValsCount := uint64(16) minIdealBalance := uint64(17) maxIdealBalance := minIdealBalance + idealValsCount - 1 idealRewards := make([]IdealAttestationReward, 0, idealValsCount) idealVals := make([]*precompute.Validator, 0, idealValsCount) increment := params.BeaconConfig().EffectiveBalanceIncrement for i := minIdealBalance; i <= maxIdealBalance; i++ { for _, v := range vals { if v.CurrentEpochEffectiveBalance/1e9 == i { effectiveBalance := i * increment idealVals = append(idealVals, &precompute.Validator{ IsActivePrevEpoch: true, IsSlashed: false, CurrentEpochEffectiveBalance: effectiveBalance, IsPrevEpochSourceAttester: true, IsPrevEpochTargetAttester: true, IsPrevEpochHeadAttester: true, }) idealRewards = append(idealRewards, IdealAttestationReward{EffectiveBalance: strconv.FormatUint(effectiveBalance, 10)}) break } } } deltas, err := altair.AttestationsDelta(st, bal, idealVals) if err != nil { http2.HandleError(w, "Could not get attestations delta: "+err.Error(), http.StatusInternalServerError) return nil, false } for i, d := range deltas { idealRewards[i].Head = strconv.FormatUint(d.HeadReward, 10) if d.SourcePenalty > 0 { idealRewards[i].Source = fmt.Sprintf("-%s", strconv.FormatUint(d.SourcePenalty, 10)) } else { idealRewards[i].Source = strconv.FormatUint(d.SourceReward, 10) } if d.TargetPenalty > 0 { idealRewards[i].Target = fmt.Sprintf("-%s", strconv.FormatUint(d.TargetPenalty, 10)) } else { idealRewards[i].Target = strconv.FormatUint(d.TargetReward, 10) } } return idealRewards, true } func totalAttRewards( w http.ResponseWriter, st state.BeaconState, bal *precompute.Balance, vals []*precompute.Validator, valIndices []primitives.ValidatorIndex, ) ([]TotalAttestationReward, bool) { totalRewards := make([]TotalAttestationReward, len(valIndices)) for i, v := range valIndices { totalRewards[i] = TotalAttestationReward{ValidatorIndex: strconv.FormatUint(uint64(v), 10)} } deltas, err := altair.AttestationsDelta(st, bal, vals) if err != nil { http2.HandleError(w, "Could not get attestations delta: "+err.Error(), http.StatusInternalServerError) return nil, false } for i, d := range deltas { totalRewards[i].Head = strconv.FormatUint(d.HeadReward, 10) if d.SourcePenalty > 0 { totalRewards[i].Source = fmt.Sprintf("-%s", strconv.FormatUint(d.SourcePenalty, 10)) } else { totalRewards[i].Source = strconv.FormatUint(d.SourceReward, 10) } if d.TargetPenalty > 0 { totalRewards[i].Target = fmt.Sprintf("-%s", strconv.FormatUint(d.TargetPenalty, 10)) } else { totalRewards[i].Target = strconv.FormatUint(d.TargetReward, 10) } } return totalRewards, true } func syncRewardsVals( w http.ResponseWriter, r *http.Request, st state.BeaconState, ) ([]*precompute.Validator, []primitives.ValidatorIndex, bool) { allVals, _, err := altair.InitializePrecomputeValidators(r.Context(), st) if err != nil { http2.HandleError(w, "Could not initialize precompute validators: "+err.Error(), http.StatusBadRequest) return nil, nil, false } valIndices, ok := requestedValIndices(w, r, st, allVals) if !ok { return nil, nil, false } sc, err := st.CurrentSyncCommittee() if err != nil { http2.HandleError(w, "Could not get current sync committee: "+err.Error(), http.StatusBadRequest) return nil, nil, false } allScIndices := make([]primitives.ValidatorIndex, len(sc.Pubkeys)) for i, pk := range sc.Pubkeys { valIdx, ok := st.ValidatorIndexByPubkey(bytesutil.ToBytes48(pk)) if !ok { http2.HandleError(w, fmt.Sprintf("No validator index found for pubkey %#x", pk), http.StatusBadRequest) return nil, nil, false } allScIndices[i] = valIdx } scIndices := make([]primitives.ValidatorIndex, 0, len(allScIndices)) scVals := make([]*precompute.Validator, 0, len(allScIndices)) for _, valIdx := range valIndices { for _, scIdx := range allScIndices { if valIdx == scIdx { scVals = append(scVals, allVals[valIdx]) scIndices = append(scIndices, valIdx) break } } } return scVals, scIndices, true } func requestedValIndices(w http.ResponseWriter, r *http.Request, st state.BeaconState, allVals []*precompute.Validator) ([]primitives.ValidatorIndex, bool) { var rawValIds []string if r.Body != http.NoBody { if err := json.NewDecoder(r.Body).Decode(&rawValIds); err != nil { http2.HandleError(w, "Could not decode validators: "+err.Error(), http.StatusBadRequest) return nil, false } } valIndices := make([]primitives.ValidatorIndex, len(rawValIds)) for i, v := range rawValIds { index, err := strconv.ParseUint(v, 10, 64) if err != nil { pubkey, err := bytesutil.FromHexString(v) if err != nil || len(pubkey) != fieldparams.BLSPubkeyLength { http2.HandleError(w, fmt.Sprintf("%s is not a validator index or pubkey", v), http.StatusBadRequest) return nil, false } var ok bool valIndices[i], ok = st.ValidatorIndexByPubkey(bytesutil.ToBytes48(pubkey)) if !ok { http2.HandleError(w, fmt.Sprintf("No validator index found for pubkey %#x", pubkey), http.StatusBadRequest) return nil, false } } else { if index >= uint64(st.NumValidators()) { http2.HandleError(w, fmt.Sprintf("Validator index %d is too large. Maximum allowed index is %d", index, st.NumValidators()-1), http.StatusBadRequest) return nil, false } valIndices[i] = primitives.ValidatorIndex(index) } } if len(valIndices) == 0 { valIndices = make([]primitives.ValidatorIndex, len(allVals)) for i := 0; i < len(allVals); i++ { valIndices[i] = primitives.ValidatorIndex(i) } } return valIndices, true }