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
1 change: 1 addition & 0 deletions cmd/dora-explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ func startFrontend(router *mux.Router) {
router.HandleFunc("/validators/summary", handlers.ValidatorsSummary).Methods("GET")
}
router.HandleFunc("/validators/activity", handlers.ValidatorsActivity).Methods("GET")
router.HandleFunc("/validators/withdrawal-dashboard", handlers.ValidatorsWithdrawalDashboard).Methods("GET")
router.HandleFunc("/validators/offline", handlers.ValidatorsOffline).Methods("GET")
router.HandleFunc("/validators/deposits", handlers.Deposits).Methods("GET")
router.HandleFunc("/validators/deposits/submit", handlers.SubmitDeposit).Methods("GET", "POST")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- +goose Up
-- +goose StatementBegin

CREATE INDEX IF NOT EXISTS "validators_withdrawal_credentials_idx"
ON public."validators" ("withdrawal_credentials");

-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'NOT SUPPORTED';
-- +goose StatementEnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- +goose Up
-- +goose StatementBegin

CREATE INDEX IF NOT EXISTS "validators_withdrawal_credentials_idx"
ON "validators" ("withdrawal_credentials");

-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'NOT SUPPORTED';
-- +goose StatementEnd
10 changes: 8 additions & 2 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2079,6 +2079,12 @@ const docTemplate = `{
"name": "status",
"in": "query"
},
{
"type": "string",
"description": "Filter by withdrawal address or withdrawal credentials",
"name": "withdrawal",
"in": "query"
},
{
"type": "string",
"description": "Sort order: index, index-d, pubkey, pubkey-d, balance, balance-d, activation, activation-d, exit, exit-d",
Expand Down Expand Up @@ -2143,13 +2149,13 @@ const docTemplate = `{
},
{
"type": "integer",
"description": "Grouping option: 1=by 100k indexes, 2=by 10k indexes, 3=by validator names (default: 3 if names available, else 1)",
"description": "Grouping option: 1=by 100k indexes, 2=by 10k indexes, 3=by validator names, 4=by withdrawal address (default: 3 if names available, else 1)",
"name": "group",
"in": "query"
},
{
"type": "string",
"description": "Search term for group names (supports regex)",
"description": "Search term for group names, withdrawal addresses, or withdrawal credentials (supports regex for non-exact address searches)",
"name": "search",
"in": "query"
},
Expand Down
10 changes: 8 additions & 2 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,12 @@
"name": "status",
"in": "query"
},
{
"type": "string",
"description": "Filter by withdrawal address or withdrawal credentials",
"name": "withdrawal",
"in": "query"
},
{
"type": "string",
"description": "Sort order: index, index-d, pubkey, pubkey-d, balance, balance-d, activation, activation-d, exit, exit-d",
Expand Down Expand Up @@ -2140,13 +2146,13 @@
},
{
"type": "integer",
"description": "Grouping option: 1=by 100k indexes, 2=by 10k indexes, 3=by validator names (default: 3 if names available, else 1)",
"description": "Grouping option: 1=by 100k indexes, 2=by 10k indexes, 3=by validator names, 4=by withdrawal address (default: 3 if names available, else 1)",
"name": "group",
"in": "query"
},
{
"type": "string",
"description": "Search term for group names (supports regex)",
"description": "Search term for group names, withdrawal addresses, or withdrawal credentials (supports regex for non-exact address searches)",
"name": "search",
"in": "query"
},
Expand Down
9 changes: 7 additions & 2 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3229,6 +3229,10 @@ paths:
in: query
name: status
type: string
- description: Filter by withdrawal address or withdrawal credentials
in: query
name: withdrawal
type: string
- description: 'Sort order: index, index-d, pubkey, pubkey-d, balance, balance-d,
activation, activation-d, exit, exit-d'
in: query
Expand Down Expand Up @@ -3273,11 +3277,12 @@ paths:
name: page
type: integer
- description: 'Grouping option: 1=by 100k indexes, 2=by 10k indexes, 3=by validator
names (default: 3 if names available, else 1)'
names, 4=by withdrawal address (default: 3 if names available, else 1)'
in: query
name: group
type: integer
- description: Search term for group names (supports regex)
- description: Search term for group names, withdrawal addresses, or withdrawal
credentials (supports regex for non-exact address searches)
in: query
name: search
type: string
Expand Down
44 changes: 12 additions & 32 deletions handlers/api/validator_withdrawalcredentials_v1.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package api

import (
"encoding/hex"
"encoding/json"
"net/http"
"strconv"
"strings"

"github.com/ethpandaops/dora/dbtypes"
"github.com/ethpandaops/dora/services"
"github.com/ethpandaops/dora/utils"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
Expand All @@ -35,56 +34,37 @@ func ApiWithdrawalCredentialsValidatorsV1(w http.ResponseWriter, r *http.Request
limitQuery := q.Get("limit")
offsetQuery := q.Get("offset")

limit, err := strconv.ParseInt(limitQuery, 10, 64)
limit, err := strconv.ParseUint(limitQuery, 10, 64)
if err != nil {
limit = 2000
}

offset, err := strconv.ParseInt(offsetQuery, 10, 64)
offset, err := strconv.ParseUint(offsetQuery, 10, 64)
if err != nil {
offset = 0
}

if offset < 0 {
offset = 0
}

if limit > (2000+offset) || limit <= 0 || limit <= offset {
limit = 2000 + offset
if limit == 0 || limit > 2000 {
limit = 2000
}

vars := mux.Vars(r)
search := vars["withdrawalCredentialsOrEth1address"]
searchBytes, err := hex.DecodeString(strings.Replace(search, "0x", "", -1))
withdrawalAddress, withdrawalCreds, err := utils.ParseWithdrawalAddressOrCredentials(search)
if err != nil {
sendBadRequestResponse(w, r.URL.String(), "invalid eth1 address provided")
sendBadRequestResponse(w, r.URL.String(), "invalid withdrawal address or credentials provided")
return
}

filter := &dbtypes.ValidatorFilter{}

if len(searchBytes) == 20 {
filter.WithdrawalAddress = searchBytes
} else {
filter.WithdrawalCreds = searchBytes
filter := &dbtypes.ValidatorFilter{
WithdrawalAddress: withdrawalAddress,
WithdrawalCreds: withdrawalCreds,
Limit: limit,
Offset: offset,
}

relevantValidators, _ := services.GlobalBeaconService.GetFilteredValidatorSet(r.Context(), filter, true)

if offset > 0 {
if int(offset) > len(relevantValidators) {
relevantValidators = relevantValidators[offset:]
} else {
relevantValidators = relevantValidators[:0]
}
}

if limit > 0 {
if len(relevantValidators) > int(limit) {
relevantValidators = relevantValidators[:limit]
}
}

data := []*ApiWithdrawalCredentialsResponseV1{}
for _, validator := range relevantValidators {
data = append(data, &ApiWithdrawalCredentialsResponseV1{
Expand Down
72 changes: 62 additions & 10 deletions handlers/api/validators_activity_v1.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"context"
"encoding/json"
"fmt"
"net/http"
Expand All @@ -10,9 +11,12 @@ import (
"strings"
"time"

"github.com/ethpandaops/dora/dbtypes"
"github.com/ethpandaops/dora/indexer/beacon"
"github.com/ethpandaops/dora/services"
"github.com/ethpandaops/dora/types/models"
"github.com/ethpandaops/dora/utils"
v1 "github.com/ethpandaops/go-eth2-client/api/v1"
"github.com/ethpandaops/go-eth2-client/spec/phase0"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -60,8 +64,8 @@ type APIValidatorActivityGroup struct {
// @Produce json
// @Param limit query int false "Number of groups to return (max 1000, default 50)"
// @Param page query int false "Page number (starts at 1)"
// @Param group query int false "Grouping option: 1=by 100k indexes, 2=by 10k indexes, 3=by validator names (default: 3 if names available, else 1)"
// @Param search query string false "Search term for group names (supports regex)"
// @Param group query int false "Grouping option: 1=by 100k indexes, 2=by 10k indexes, 3=by validator names, 4=by withdrawal address (default: 3 if names available, else 1)"
// @Param search query string false "Search term for group names, withdrawal addresses, or withdrawal credentials (supports regex for non-exact address searches)"
// @Param order query string false "Sort order: group, group-d, count, count-d, active, active-d, online, online-d, offline, offline-d, exited, exited-d, slashed, slashed-d (default: group)"
// @Success 200 {object} APIValidatorsActivityResponse
// @Failure 400 {object} map[string]string "Invalid parameters"
Expand Down Expand Up @@ -125,8 +129,8 @@ func APIValidatorsActivityV1(w http.ResponseWriter, r *http.Request) {
groupBy = 1
}
}
if groupBy < 1 || groupBy > 3 {
http.Error(w, `{"status": "ERROR: group parameter must be 1, 2, or 3"}`, http.StatusBadRequest)
if groupBy < 1 || groupBy > 4 {
http.Error(w, `{"status": "ERROR: group parameter must be 1, 2, 3, or 4"}`, http.StatusBadRequest)
return
}

Expand Down Expand Up @@ -240,8 +244,15 @@ func buildValidatorsActivityAPIData(pageIdx uint64, pageSize uint64, sortOrder s
// Group validators
validatorGroupMap := map[string]*models.ValidatorsActiviyPageDataGroup{}
currentEpoch := services.GlobalBeaconService.GetChainState().CurrentEpoch()
var withdrawalAddressFilter []byte
var withdrawalCredsFilter []byte
exactWithdrawalSearch := false
if groupBy == 4 && searchTerm != "" {
withdrawalAddressFilter, withdrawalCredsFilter, _ = utils.ParseWithdrawalAddressOrCredentials(searchTerm)
exactWithdrawalSearch = len(withdrawalAddressFilter) > 0 || len(withdrawalCredsFilter) > 0
}

services.GlobalBeaconService.StreamActiveValidatorData(false, func(index phase0.ValidatorIndex, validatorFlags uint16, activeData *beacon.ValidatorData, validator *phase0.Validator) error {
addGroupValidator := func(index phase0.ValidatorIndex, validatorFlags uint16, activeData *beacon.ValidatorData, validator *phase0.Validator) {
var groupKey string
var groupName string

Expand All @@ -257,8 +268,15 @@ func buildValidatorsActivityAPIData(pageIdx uint64, pageSize uint64, sortOrder s
case 3:
groupName = services.GlobalBeaconService.GetValidatorName(uint64(index))
groupKey = strings.ToLower(groupName)
case 4:
if validator != nil {
groupKey, groupName = utils.WithdrawalCredentialsGroup(validator.WithdrawalCredentials)
}
}

if groupKey == "" {
groupKey = "unknown"
}
validatorGroup := validatorGroupMap[groupKey]
if validatorGroup == nil {
validatorGroup = &models.ValidatorsActiviyPageDataGroup{
Expand Down Expand Up @@ -301,16 +319,48 @@ func buildValidatorsActivityAPIData(pageIdx uint64, pageSize uint64, sortOrder s
if isExited {
validatorGroup.Exited++
}
}

return nil
})
if groupBy == 4 && exactWithdrawalSearch {
filteredValidators, _ := services.GlobalBeaconService.GetFilteredValidatorSet(context.Background(), &dbtypes.ValidatorFilter{
WithdrawalAddress: withdrawalAddressFilter,
WithdrawalCreds: withdrawalCredsFilter,
OrderBy: dbtypes.ValidatorOrderIndexAsc,
}, false)
for _, validator := range filteredValidators {
if validator.Validator == nil {
continue
}

flags := uint16(0)
if validator.Validator.Slashed {
flags |= beacon.ValidatorStatusSlashed
}
activeData := (*beacon.ValidatorData)(nil)
if validator.Status == v1.ValidatorStateActiveOngoing || validator.Status == v1.ValidatorStateActiveExiting || validator.Status == v1.ValidatorStateActiveSlashed {
activeData = &beacon.ValidatorData{
ActivationEpoch: validator.Validator.ActivationEpoch,
ExitEpoch: validator.Validator.ExitEpoch,
}
} else if validator.Status == v1.ValidatorStateExitedUnslashed || validator.Status == v1.ValidatorStateExitedSlashed {
flags |= beacon.ValidatorStatusExited
}

addGroupValidator(validator.Index, flags, activeData, validator.Validator)
}
} else {
services.GlobalBeaconService.StreamActiveValidatorData(false, func(index phase0.ValidatorIndex, validatorFlags uint16, activeData *beacon.ValidatorData, validator *phase0.Validator) error {
addGroupValidator(index, validatorFlags, activeData, validator)
return nil
})
}

// Filter groups based on search term
validatorGroups := []*models.ValidatorsActiviyPageDataGroup{}

// Check if search term is a valid regex pattern
var searchRegex *regexp.Regexp
if searchTerm != "" {
if searchTerm != "" && !exactWithdrawalSearch {
// Try to compile as regex
var err error
searchRegex, err = regexp.Compile("(?i)" + searchTerm) // Case-insensitive regex
Expand All @@ -322,7 +372,7 @@ func buildValidatorsActivityAPIData(pageIdx uint64, pageSize uint64, sortOrder s

for _, group := range validatorGroupMap {
// Apply search filter
if searchTerm != "" {
if searchTerm != "" && !exactWithdrawalSearch {
matched := false

if searchRegex != nil {
Expand Down Expand Up @@ -421,7 +471,9 @@ func buildValidatorsActivityAPIData(pageIdx uint64, pageSize uint64, sortOrder s
if groupCount%pageSize != 0 {
pageData.TotalPages++
}
pageData.LastPageIndex = pageData.TotalPages - 1
if pageData.TotalPages > 0 {
pageData.LastPageIndex = pageData.TotalPages - 1
}
pageData.FirstGroup = startIdx
pageData.LastGroup = endIdx

Expand Down
13 changes: 13 additions & 0 deletions handlers/api/validators_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/ethpandaops/dora/dbtypes"
"github.com/ethpandaops/dora/services"
"github.com/ethpandaops/dora/utils"
v1 "github.com/ethpandaops/go-eth2-client/api/v1"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -63,6 +64,7 @@ type APIValidatorInfo struct {
// @Param index query string false "Filter by validator index"
// @Param name query string false "Filter by validator name"
// @Param status query string false "Filter by validator status (comma-separated)"
// @Param withdrawal query string false "Filter by withdrawal address or withdrawal credentials"
// @Param order query string false "Sort order: index, index-d, pubkey, pubkey-d, balance, balance-d, activation, activation-d, exit, exit-d"
// @Success 200 {object} APIValidatorsResponse
// @Failure 400 {object} map[string]string "Invalid parameters"
Expand Down Expand Up @@ -113,6 +115,7 @@ func APIValidatorsV1(w http.ResponseWriter, r *http.Request) {
filterIndex := query.Get("index")
filterName := query.Get("name")
filterStatus := query.Get("status")
filterWithdrawal := query.Get("withdrawal")
sortOrder := query.Get("order")

// Build validator filter
Expand Down Expand Up @@ -160,6 +163,16 @@ func APIValidatorsV1(w http.ResponseWriter, r *http.Request) {
}
}

if filterWithdrawal != "" {
withdrawalAddress, withdrawalCreds, err := utils.ParseWithdrawalAddressOrCredentials(filterWithdrawal)
if err != nil {
http.Error(w, `{"status": "ERROR: invalid withdrawal address or credentials"}`, http.StatusBadRequest)
return
}
validatorFilter.WithdrawalAddress = withdrawalAddress
validatorFilter.WithdrawalCreds = withdrawalCreds
}

// Apply sort order
switch sortOrder {
case "index-d":
Expand Down
Loading