Skip to content
Open
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
40 changes: 30 additions & 10 deletions api/handlers/validators/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import (
"github.com/ssvlabs/ssv/protocol/v2/types"
)

// requestClusters is a space-separated list of comma-separated lists of operator IDs.
type requestClusters [][]uint64
// Clusters represents clusters of operator IDs.
//
// Query format: space-separated list of comma-separated operator IDs (e.g. "1,2,3,4 5,6,7,8").
// JSON format: array of arrays (e.g. [[1,2,3,4],[5,6,7,8]]).
type Clusters [][]uint64

func (c *requestClusters) Bind(value string) error {
func (c *Clusters) Bind(value string) error {
if value == "" {
return nil
}
Expand All @@ -32,22 +35,39 @@ func (c *requestClusters) Bind(value string) error {
return nil
}

type validatorJSON struct {
// ValidatorsRequest represents the filters accepted by the validators endpoint.
type ValidatorsRequest struct {
Owners api.HexSlice `json:"owners" form:"owners" swaggertype:"array,string" format:"hex"`
Operators api.Uint64Slice `json:"operators" form:"operators" swaggertype:"array,integer" format:"int64" minimum:"0"`
Clusters Clusters `json:"clusters" form:"clusters"`
Subclusters Clusters `json:"subclusters" form:"subclusters"`
PubKeys api.HexSlice `json:"pubkeys" form:"pubkeys" swaggertype:"array,string" format:"hex"`
Indices api.Uint64Slice `json:"indices" form:"indices" swaggertype:"array,integer" format:"int64" minimum:"0"`
Pagination api.PaginationRequest `json:"pagination" form:"pagination"`
}

// ValidatorsResponse represents the response from the validators endpoint.
type ValidatorsResponse struct {
Data []*Validator `json:"data"`
Pagination *api.PaginationResponse `json:"pagination,omitempty"`
}

type Validator struct {
PubKey api.Hex `json:"public_key"`
Index phase0.ValidatorIndex `json:"index"`
Index phase0.ValidatorIndex `json:"index" swaggertype:"string" example:"123"`
Status string `json:"status"`
ActivationEpoch phase0.Epoch `json:"activation_epoch"`
ExitEpoch phase0.Epoch `json:"exit_epoch"`
ActivationEpoch phase0.Epoch `json:"activation_epoch" swaggertype:"string" example:"0"`
ExitEpoch phase0.Epoch `json:"exit_epoch" swaggertype:"string" example:"0"`
Owner api.Hex `json:"owner"`
Committee []spectypes.OperatorID `json:"committee"`
Committee []spectypes.OperatorID `json:"committee" swaggertype:"array,integer" format:"int64" minimum:"0"`
Quorum uint64 `json:"quorum"`
PartialQuorum uint64 `json:"partial_quorum"`
Graffiti string `json:"graffiti"`
Liquidated bool `json:"liquidated"`
}

func validatorFromShare(share *types.SSVShare) *validatorJSON {
v := &validatorJSON{
func validatorFromShare(share *types.SSVShare) *Validator {
v := &Validator{
PubKey: api.Hex(share.ValidatorPubKey[:]),
Owner: api.Hex(share.OwnerAddress[:]),
Committee: func() []spectypes.OperatorID {
Expand Down
74 changes: 60 additions & 14 deletions api/handlers/validators/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package validators

import (
"bytes"
"fmt"
"net/http"
"sort"
"strconv"
"strings"

Expand All @@ -17,22 +19,41 @@ type Validators struct {
Shares registrystorage.Shares
}

// List godoc
// @Summary Get validators
// @Description Returns the list of validators managed by the SSV node. Pagination is provided via the JSON request body under "pagination".
// @Tags Validators
// @Accept json
// @Produce json
// @Param request body ValidatorsRequest false "Filters and pagination as JSON body"
// @Success 200 {object} ValidatorsResponse
// @Failure 400 {object} api.ErrorResponse
// @Failure 429 {object} api.ErrorResponse "Too Many Requests"
// @Failure 500 {object} api.ErrorResponse
// @Router /v1/validators [get]
func (h *Validators) List(w http.ResponseWriter, r *http.Request) error {
var request struct {
Owners api.HexSlice `json:"owners" form:"owners"`
Operators api.Uint64Slice `json:"operators" form:"operators"`
Clusters requestClusters `json:"clusters" form:"clusters"`
Subclusters requestClusters `json:"subclusters" form:"subclusters"`
PubKeys api.HexSlice `json:"pubkeys" form:"pubkeys"`
Indices api.Uint64Slice `json:"indices" form:"indices"`
}
var response struct {
Data []*validatorJSON `json:"data"`
const (
defaultPerPage = uint64(1000)
maxPerPage = uint64(10000)
)

if r.URL.Query().Has("page") || r.URL.Query().Has("per_page") || r.URL.Query().Has("pagination") {
return api.BadRequestError(fmt.Errorf("pagination must be provided in request body"))
}

var request ValidatorsRequest
if err := api.Bind(r, &request); err != nil {
return err
return api.BadRequestError(err)
}

pagination, err := request.Pagination.ToPagination(api.PaginationOptions{
DefaultPerPage: defaultPerPage,
MaxPerPage: maxPerPage,
})
if err != nil {
return api.BadRequestError(err)
}
paginationRequested := pagination != nil

var filters []registrystorage.SharesFilter
if len(request.Owners) > 0 {
Expand All @@ -55,10 +76,35 @@ func (h *Validators) List(w http.ResponseWriter, r *http.Request) error {
}

shares := h.Shares.List(nil, filters...)
response.Data = make([]*validatorJSON, len(shares))
for i, share := range shares {

var response ValidatorsResponse

// if no pagination requested, return retro-compatible response without pagination metadata
if !paginationRequested {
response.Data = make([]*Validator, len(shares))
for i, share := range shares {
response.Data[i] = validatorFromShare(share)
}
return api.Render(w, r, response)
}

// Ensure deterministic ordering for pagination.
sort.Slice(shares, func(i, j int) bool {
return bytes.Compare(shares[i].ValidatorPubKey[:], shares[j].ValidatorPubKey[:]) < 0
})

total := uint64(len(shares))
start, end := pagination.SliceBounds(total)

pagedShares := shares[start:end]
response.Data = make([]*Validator, len(pagedShares))
for i, share := range pagedShares {
response.Data[i] = validatorFromShare(share)
}

p := api.PaginationResponseFromPagination(pagination, total)
response.Pagination = &p

return api.Render(w, r, response)
}

Expand Down Expand Up @@ -87,7 +133,7 @@ func byOperators(operators []uint64) registrystorage.SharesFilter {
}

// byClusters returns a filter that matches shares that match or contain any of the given clusters.
func byClusters(clusters requestClusters, contains bool) registrystorage.SharesFilter {
func byClusters(clusters Clusters, contains bool) registrystorage.SharesFilter {
return func(share *types.SSVShare) bool {
shareCommittee := make([]string, len(share.Committee))
for i, c := range share.Committee {
Expand Down
Loading