erigon-pulse/cl/beacon/handler/validators.go
2024-01-14 23:22:34 -06:00

544 lines
17 KiB
Go

package handler
import (
"encoding/hex"
"fmt"
"math"
"net/http"
"strconv"
"strings"
libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/erigon/cl/beacon/beaconhttp"
"github.com/ledgerwatch/erigon/cl/cltypes/solid"
"github.com/ledgerwatch/erigon/cl/persistence/beacon_indicies"
state_accessors "github.com/ledgerwatch/erigon/cl/persistence/state"
"github.com/ledgerwatch/erigon/cl/phase1/core/state"
"golang.org/x/exp/slices"
)
type validatorStatus int
var validatorJsonTemplate = "{\"index\":\"%d\",\"status\":\"%s\",\"balance\":\"%d\",\"validator\":{\"pubkey\":\"0x%x\",\"withdrawal_credentials\":\"0x%x\",\"effective_balance\":\"%d\",\"slashed\":%t,\"activation_eligibility_epoch\":\"%d\",\"activation_epoch\":\"%d\",\"exit_epoch\":\"%d\",\"withdrawable_epoch\":\"%d\"}}"
const (
validatorPendingInitialized validatorStatus = 1 //"pending_initialized"
validatorPendingQueued validatorStatus = 2 //"pending_queued"
validatorActiveOngoing validatorStatus = 3 //"active_ongoing"
validatorActiveExiting validatorStatus = 4 //"active_exiting"
validatorActiveSlashed validatorStatus = 5 //"active_slashed"
validatorExitedUnslashed validatorStatus = 6 //"exited_unslashed"
validatorExitedSlashed validatorStatus = 7 //"exited_slashed"
validatorWithdrawalPossible validatorStatus = 8 //"withdrawal_possible"
validatorWithdrawalDone validatorStatus = 9 //"withdrawal_done"
validatorActive validatorStatus = 10 //"active"
validatorPending validatorStatus = 11 //"pending"
validatorExited validatorStatus = 12 //"exited"
validatorWithdrawal validatorStatus = 13 //"withdrawal"
)
func validatorStatusFromString(s string) (validatorStatus, error) {
switch s {
case "pending_initialized":
return validatorPendingInitialized, nil
case "pending_queued":
return validatorPendingQueued, nil
case "active_ongoing":
return validatorActiveOngoing, nil
case "active_exiting":
return validatorActiveExiting, nil
case "active_slashed":
return validatorActiveSlashed, nil
case "exited_unslashed":
return validatorExitedUnslashed, nil
case "exited_slashed":
return validatorExitedSlashed, nil
case "withdrawal_possible":
return validatorWithdrawalPossible, nil
case "withdrawal_done":
return validatorWithdrawalDone, nil
case "active":
return validatorActive, nil
case "pending":
return validatorPending, nil
case "exited":
return validatorExited, nil
case "withdrawal":
return validatorWithdrawal, nil
default:
return 0, fmt.Errorf("invalid validator status %s", s)
}
}
func validatorStatusFromValidator(v solid.Validator, currentEpoch uint64, balance uint64) validatorStatus {
activationEpoch := v.ActivationEpoch()
// pending section
if activationEpoch > currentEpoch {
activationEligibilityEpoch := v.ActivationEligibilityEpoch()
if activationEligibilityEpoch == math.MaxUint64 {
return validatorPendingInitialized
}
return validatorPendingQueued
}
exitEpoch := v.ExitEpoch()
// active section
if activationEpoch <= currentEpoch && currentEpoch < exitEpoch {
if exitEpoch == math.MaxUint64 {
return validatorActiveOngoing
}
slashed := v.Slashed()
if slashed {
return validatorActiveSlashed
}
return validatorActiveExiting
}
withdrawableEpoch := v.WithdrawableEpoch()
// exited section
if exitEpoch <= currentEpoch && currentEpoch < withdrawableEpoch {
if v.Slashed() {
return validatorExitedSlashed
}
return validatorExitedUnslashed
}
if balance == 0 {
return validatorWithdrawalDone
}
return validatorWithdrawalPossible
}
func (s validatorStatus) String() string {
switch s {
case validatorPendingInitialized:
return "pending_initialized"
case validatorPendingQueued:
return "pending_queued"
case validatorActiveOngoing:
return "active_ongoing"
case validatorActiveExiting:
return "active_exiting"
case validatorActiveSlashed:
return "active_slashed"
case validatorExitedUnslashed:
return "exited_unslashed"
case validatorExitedSlashed:
return "exited_slashed"
case validatorWithdrawalPossible:
return "withdrawal_possible"
case validatorWithdrawalDone:
return "withdrawal_done"
case validatorActive:
return "active"
case validatorPending:
return "pending"
case validatorExited:
return "exited"
case validatorWithdrawal:
return "withdrawal"
default:
panic("invalid validator status")
}
}
const maxValidatorsLookupFilter = 32
func parseStatuses(s []string) ([]validatorStatus, error) {
seenAlready := make(map[validatorStatus]struct{})
statuses := make([]validatorStatus, 0, len(s))
if len(s) > maxValidatorsLookupFilter {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, "too many statuses requested")
}
for _, status := range s {
s, err := validatorStatusFromString(status)
if err != nil {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
if _, ok := seenAlready[s]; ok {
continue
}
seenAlready[s] = struct{}{}
statuses = append(statuses, s)
}
return statuses, nil
}
func checkValidValidatorId(s string) (bool, error) {
// If it starts with 0x, then it must a 48bytes 0x prefixed string
if len(s) == 98 && s[:2] == "0x" {
// check if it is a valid hex string
if _, err := hex.DecodeString(s[2:]); err != nil {
return false, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
return true, nil
}
// If it is not 0x prefixed, then it must be a number, check if it is a base-10 number
if _, err := strconv.ParseUint(s, 10, 64); err != nil {
return false, beaconhttp.NewEndpointError(http.StatusBadRequest, "invalid validator id")
}
return false, nil
}
func (a *ApiHandler) getAllValidators(w http.ResponseWriter, r *http.Request) (*beaconhttp.BeaconResponse, error) {
ctx := r.Context()
tx, err := a.indiciesDB.BeginRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
blockId, err := beaconhttp.StateIdFromRequest(r)
if err != nil {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
blockRoot, httpStatus, err := a.blockRootFromStateId(ctx, tx, blockId)
if err != nil {
return nil, beaconhttp.NewEndpointError(httpStatus, err.Error())
}
queryFilters, err := beaconhttp.StringListFromQueryParams(r, "status")
if err != nil {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
validatorIds, err := beaconhttp.StringListFromQueryParams(r, "id")
if err != nil {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
if len(validatorIds) > maxValidatorsLookupFilter {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, "too many validators requested")
}
filterIndicies, err := parseQueryValidatorIndicies(tx, validatorIds)
if err != nil {
return nil, err
}
// Check the filters' validity
statusFilters, err := parseStatuses(queryFilters)
if err != nil {
return nil, err
}
if blockId.Head() { // Lets see if we point to head, if yes then we need to look at the head state we always keep.
s, cn := a.syncedData.HeadState()
defer cn()
if s == nil {
return nil, beaconhttp.NewEndpointError(http.StatusNotFound, "node is not synced")
}
return responseValidators(filterIndicies, statusFilters, state.Epoch(s), s.Balances(), s.Validators(), false)
}
slot, err := beacon_indicies.ReadBlockSlotByBlockRoot(tx, blockRoot)
if err != nil {
return nil, err
}
if slot == nil {
return nil, beaconhttp.NewEndpointError(http.StatusNotFound, "state not found")
}
stateEpoch := *slot / a.beaconChainCfg.SlotsPerEpoch
state, err := a.forkchoiceStore.GetStateAtBlockRoot(blockRoot, true)
if err != nil {
return nil, err
}
if state == nil {
validatorSet, err := a.stateReader.ReadValidatorsForHistoricalState(tx, *slot)
if err != nil {
return nil, err
}
balances, err := a.stateReader.ReadValidatorsBalances(tx, *slot)
if err != nil {
return nil, err
}
return responseValidators(filterIndicies, statusFilters, stateEpoch, balances, validatorSet, true)
}
return responseValidators(filterIndicies, statusFilters, stateEpoch, state.Balances(), state.Validators(), *slot <= a.forkchoiceStore.FinalizedSlot())
}
func parseQueryValidatorIndex(tx kv.Tx, id string) (uint64, error) {
isPublicKey, err := checkValidValidatorId(id)
if err != nil {
return 0, err
}
if isPublicKey {
var b48 libcommon.Bytes48
if err := b48.UnmarshalText([]byte(id)); err != nil {
return 0, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
has, err := tx.Has(kv.InvertedValidatorPublicKeys, b48[:])
if err != nil {
return 0, err
}
if !has {
return math.MaxUint64, nil
}
idx, ok, err := state_accessors.ReadValidatorIndexByPublicKey(tx, b48)
if err != nil {
return 0, err
}
if !ok {
return 0, beaconhttp.NewEndpointError(http.StatusNotFound, "validator not found")
}
return idx, nil
}
idx, err := strconv.ParseUint(id, 10, 64)
if err != nil {
return 0, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
return idx, nil
}
func parseQueryValidatorIndicies(tx kv.Tx, ids []string) ([]uint64, error) {
filterIndicies := make([]uint64, 0, len(ids))
for _, id := range ids {
idx, err := parseQueryValidatorIndex(tx, id)
if err != nil {
return nil, err
}
filterIndicies = append(filterIndicies, idx)
}
return filterIndicies, nil
}
func (a *ApiHandler) getSingleValidator(w http.ResponseWriter, r *http.Request) (*beaconhttp.BeaconResponse, error) {
ctx := r.Context()
tx, err := a.indiciesDB.BeginRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
blockId, err := beaconhttp.StateIdFromRequest(r)
if err != nil {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
blockRoot, httpStatus, err := a.blockRootFromStateId(ctx, tx, blockId)
if err != nil {
return nil, beaconhttp.NewEndpointError(httpStatus, err.Error())
}
validatorId, err := beaconhttp.StringFromRequest(r, "validator_id")
if err != nil {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
validatorIndex, err := parseQueryValidatorIndex(tx, validatorId)
if err != nil {
return nil, err
}
if blockId.Head() { // Lets see if we point to head, if yes then we need to look at the head state we always keep.
s, cn := a.syncedData.HeadState()
defer cn()
if s.ValidatorLength() <= int(validatorIndex) {
return newBeaconResponse([]int{}).WithFinalized(false), nil
}
if s == nil {
return nil, beaconhttp.NewEndpointError(http.StatusNotFound, "node is not synced")
}
return responseValidator(validatorIndex, state.Epoch(s), s.Balances(), s.Validators(), false)
}
slot, err := beacon_indicies.ReadBlockSlotByBlockRoot(tx, blockRoot)
if err != nil {
return nil, err
}
if slot == nil {
return nil, beaconhttp.NewEndpointError(http.StatusNotFound, "state not found")
}
stateEpoch := *slot / a.beaconChainCfg.SlotsPerEpoch
state, err := a.forkchoiceStore.GetStateAtBlockRoot(blockRoot, true)
if err != nil {
return nil, err
}
if state == nil {
validatorSet, err := a.stateReader.ReadValidatorsForHistoricalState(tx, *slot)
if err != nil {
return nil, err
}
balances, err := a.stateReader.ReadValidatorsBalances(tx, *slot)
if err != nil {
return nil, err
}
return responseValidator(validatorIndex, stateEpoch, balances, validatorSet, true)
}
return responseValidator(validatorIndex, stateEpoch, state.Balances(), state.Validators(), *slot <= a.forkchoiceStore.FinalizedSlot())
}
func (a *ApiHandler) getAllValidatorsBalances(w http.ResponseWriter, r *http.Request) (*beaconhttp.BeaconResponse, error) {
ctx := r.Context()
tx, err := a.indiciesDB.BeginRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
blockId, err := beaconhttp.StateIdFromRequest(r)
if err != nil {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
blockRoot, httpStatus, err := a.blockRootFromStateId(ctx, tx, blockId)
if err != nil {
return nil, beaconhttp.NewEndpointError(httpStatus, err.Error())
}
validatorIds, err := beaconhttp.StringListFromQueryParams(r, "id")
if err != nil {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error())
}
if len(validatorIds) > maxValidatorsLookupFilter {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, "too many validators requested")
}
filterIndicies, err := parseQueryValidatorIndicies(tx, validatorIds)
if err != nil {
return nil, err
}
if blockId.Head() { // Lets see if we point to head, if yes then we need to look at the head state we always keep.
s, cn := a.syncedData.HeadState()
defer cn()
if s == nil {
return nil, beaconhttp.NewEndpointError(http.StatusNotFound, "node is not synced")
}
return responseValidatorsBalances(filterIndicies, state.Epoch(s), s.Balances(), false)
}
slot, err := beacon_indicies.ReadBlockSlotByBlockRoot(tx, blockRoot)
if err != nil {
return nil, err
}
if slot == nil {
return nil, beaconhttp.NewEndpointError(http.StatusNotFound, "state not found")
}
stateEpoch := *slot / a.beaconChainCfg.SlotsPerEpoch
state, err := a.forkchoiceStore.GetStateAtBlockRoot(blockRoot, true)
if err != nil {
return nil, err
}
if state == nil {
balances, err := a.stateReader.ReadValidatorsBalances(tx, *slot)
if err != nil {
return nil, err
}
return responseValidatorsBalances(filterIndicies, stateEpoch, balances, true)
}
return responseValidatorsBalances(filterIndicies, stateEpoch, state.Balances(), *slot <= a.forkchoiceStore.FinalizedSlot())
}
type directString string
func (d directString) MarshalJSON() ([]byte, error) {
return []byte(d), nil
}
func responseValidators(filterIndicies []uint64, filterStatuses []validatorStatus, stateEpoch uint64, balances solid.Uint64ListSSZ, validators *solid.ValidatorSet, finalized bool) (*beaconhttp.BeaconResponse, error) {
var b strings.Builder
b.WriteString("[")
first := true
var err error
validators.Range(func(i int, v solid.Validator, l int) bool {
if len(filterIndicies) > 0 && !slices.Contains(filterIndicies, uint64(i)) {
return true
}
status := validatorStatusFromValidator(v, stateEpoch, balances.Get(i))
if shouldStatusBeFiltered(status, filterStatuses) {
return true
}
if !first {
if _, err = b.WriteString(","); err != nil {
return false
}
}
first = false
if _, err = b.WriteString(fmt.Sprintf(validatorJsonTemplate, i, status.String(), balances.Get(i), v.PublicKey(), v.WithdrawalCredentials(), v.EffectiveBalance(), v.Slashed(), v.ActivationEligibilityEpoch(), v.ActivationEpoch(), v.ExitEpoch(), v.WithdrawableEpoch())); err != nil {
return false
}
return true
})
if err != nil {
return nil, err
}
_, err = b.WriteString("]\n")
return newBeaconResponse(directString(b.String())).WithFinalized(finalized), err
}
func responseValidator(idx uint64, stateEpoch uint64, balances solid.Uint64ListSSZ, validators *solid.ValidatorSet, finalized bool) (*beaconhttp.BeaconResponse, error) {
var b strings.Builder
var err error
if validators.Length() <= int(idx) {
return newBeaconResponse([]int{}).WithFinalized(finalized), nil
}
v := validators.Get(int(idx))
status := validatorStatusFromValidator(v, stateEpoch, balances.Get(int(idx)))
if _, err = b.WriteString(fmt.Sprintf(validatorJsonTemplate, idx, status.String(), balances.Get(int(idx)), v.PublicKey(), v.WithdrawalCredentials(), v.EffectiveBalance(), v.Slashed(), v.ActivationEligibilityEpoch(), v.ActivationEpoch(), v.ExitEpoch(), v.WithdrawableEpoch())); err != nil {
return nil, err
}
_, err = b.WriteString("\n")
return newBeaconResponse(directString(b.String())).WithFinalized(finalized), err
}
func responseValidatorsBalances(filterIndicies []uint64, stateEpoch uint64, balances solid.Uint64ListSSZ, finalized bool) (*beaconhttp.BeaconResponse, error) {
var b strings.Builder
b.WriteString("[")
jsonTemplate := "{\"index\":\"%d\",\"balance\":\"%d\"}"
first := true
var err error
balances.Range(func(i int, v uint64, l int) bool {
if len(filterIndicies) > 0 && !slices.Contains(filterIndicies, uint64(i)) {
return true
}
if !first {
if _, err = b.WriteString(","); err != nil {
return false
}
}
first = false
if _, err = b.WriteString(fmt.Sprintf(jsonTemplate, i, v)); err != nil {
return false
}
return true
})
if err != nil {
return nil, err
}
_, err = b.WriteString("]\n")
return newBeaconResponse(directString(b.String())).WithFinalized(finalized), err
}
func shouldStatusBeFiltered(status validatorStatus, statuses []validatorStatus) bool {
if len(statuses) == 0 {
return false
}
for _, s := range statuses {
if (s == status) || (s == validatorActive && (status == validatorActiveOngoing || status == validatorActiveExiting || status == validatorActiveSlashed)) ||
(s == validatorPending && (status == validatorPendingInitialized || status == validatorPendingQueued)) ||
(s == validatorExited && (status == validatorExitedUnslashed || status == validatorExitedSlashed)) ||
(s == validatorWithdrawal && (status == validatorWithdrawalPossible || status == validatorWithdrawalDone)) {
return false
}
}
return true // filter if no filter condition is met
}