Skip to content

Commit

Permalink
added validator activity page (/validators/activity)
Browse files Browse the repository at this point in the history
  • Loading branch information
pk910 committed Jan 24, 2024
1 parent d47c9c7 commit b24339a
Show file tree
Hide file tree
Showing 7 changed files with 545 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/dora-explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func startFrontend() {
router.HandleFunc("/search", handlers.Search).Methods("GET")
router.HandleFunc("/search/{type}", handlers.SearchAhead).Methods("GET")
router.HandleFunc("/validators", handlers.Validators).Methods("GET")
router.HandleFunc("/validators/activity", handlers.ValidatorsActivity).Methods("GET")
router.HandleFunc("/validator/{idxOrPubKey}", handlers.Validator).Methods("GET")
router.HandleFunc("/validator/{index}/slots", handlers.ValidatorSlots).Methods("GET")

Expand Down
5 changes: 5 additions & 0 deletions handlers/pageData.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ func createMenuItems(active string, isMain bool) []types.MainMenuItem {
Path: "/validators",
Icon: "fa-table",
},
{
Label: "Validator Activity",
Path: "/validators/activity",
Icon: "fa-tachometer",
},
},
},
{
Expand Down
276 changes: 276 additions & 0 deletions handlers/validators_activity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
package handlers

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

"github.com/ethereum/go-ethereum/common"
"github.com/pk910/dora/services"
"github.com/pk910/dora/templates"
"github.com/pk910/dora/types/models"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
)

// ValidatorsActivity will return the filtered "slots" page using a go template
func ValidatorsActivity(w http.ResponseWriter, r *http.Request) {
var pageTemplateFiles = append(layoutTemplateFiles,
"validators_activity/validators_activity.html",
"_svg/professor.html",
)

var pageTemplate = templates.GetTemplate(pageTemplateFiles...)
data := InitPageData(w, r, "validators", "/validators/activity", "Validators Activity", pageTemplateFiles)

urlArgs := r.URL.Query()
var pageSize uint64 = 50
if urlArgs.Has("c") {
pageSize, _ = strconv.ParseUint(urlArgs.Get("c"), 10, 64)
}
var pageIdx uint64 = 0
if urlArgs.Has("s") {
pageIdx, _ = strconv.ParseUint(urlArgs.Get("s"), 10, 64)
}

var sortOrder string
if urlArgs.Has("o") {
sortOrder = urlArgs.Get("o")
}
if sortOrder == "" {
sortOrder = "group"
}

var groupBy uint64
if urlArgs.Has("group") {
groupBy, _ = strconv.ParseUint(urlArgs.Get("group"), 10, 64)
}
if groupBy == 0 {
if services.GlobalBeaconService.GetValidatorNamesCount() > 0 {
groupBy = 3
} else {
groupBy = 1
}
}

var pageError error
pageError = services.GlobalCallRateLimiter.CheckCallLimit(r, 2)
if pageError == nil {
data.Data, pageError = getValidatorsActivityPageData(pageIdx, pageSize, sortOrder, groupBy)
}
if pageError != nil {
handlePageError(w, r, pageError)
return
}
w.Header().Set("Content-Type", "text/html")
if handleTemplateError(w, r, "slots_filtered.go", "SlotsFiltered", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil {
return // an error has occurred and was processed
}
}

func getValidatorsActivityPageData(pageIdx uint64, pageSize uint64, sortOrder string, groupBy uint64) (*models.ValidatorsActivityPageData, error) {
pageData := &models.ValidatorsActivityPageData{}
pageCacheKey := fmt.Sprintf("validators_activiy:%v:%v:%v:%v", pageIdx, pageSize, sortOrder, groupBy)
pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(_ *services.FrontendCacheProcessingPage) interface{} {
return buildValidatorsActivityPageData(pageIdx, pageSize, sortOrder, groupBy)
})
if pageErr == nil && pageRes != nil {
resData, resOk := pageRes.(*models.ValidatorsActivityPageData)
if !resOk {
return nil, ErrInvalidPageModel
}
pageData = resData
}
return pageData, pageErr
}

func buildValidatorsActivityPageData(pageIdx uint64, pageSize uint64, sortOrder string, groupBy uint64) *models.ValidatorsActivityPageData {
filterArgs := url.Values{}
filterArgs.Add("group", fmt.Sprintf("%v", groupBy))

pageData := &models.ValidatorsActivityPageData{
ViewOptionGroupBy: groupBy,
Sorting: sortOrder,
}
logrus.Debugf("validators_activity page called: %v:%v [%v]", pageIdx, pageSize, groupBy)
if pageIdx == 0 {
pageData.IsDefaultPage = true
}

if pageSize > 100 {
pageSize = 100
}
pageData.PageSize = pageSize
pageData.CurrentPageIndex = pageIdx + 1
if pageIdx >= 1 {
pageData.PrevPageIndex = pageIdx
}
pageData.LastPageIndex = 0

// group validators
validatorGroupMap := map[string]*models.ValidatorsActiviyPageDataGroup{}
validatorSet := services.GlobalBeaconService.GetCachedValidatorSet()
activityMap, _ := services.GlobalBeaconService.GetValidatorActivity()

for vIdx, validator := range validatorSet {
var groupKey string
var groupName string

switch groupBy {
case 1:
groupIdx := uint64(vIdx) / 100000
groupKey = fmt.Sprintf("%06d", groupIdx)
groupName = fmt.Sprintf("%v - %v", groupIdx*100000, (groupIdx+1)*100000)
case 2:
groupIdx := uint64(vIdx) / 10000
groupKey = fmt.Sprintf("%06d", groupIdx)
groupName = fmt.Sprintf("%v - %v", groupIdx*10000, (groupIdx+1)*10000)
case 3:
groupName = services.GlobalBeaconService.GetValidatorName(uint64(vIdx))
groupKey = strings.ToLower(groupName)
case 4:
if validator.Validator.WithdrawalCredentials[0] == 0x01 {
groupName = common.BytesToAddress(validator.Validator.WithdrawalCredentials[12:]).Hex()
groupKey = strings.ToLower(groupName)
}
}

validatorGroup := validatorGroupMap[groupKey]
if validatorGroup == nil {
validatorGroup = &models.ValidatorsActiviyPageDataGroup{
Group: groupName,
GroupLower: groupKey,
Validators: 0,
Activated: 0,
Online: 0,
Offline: 0,
Exited: 0,
Slashed: 0,
}
validatorGroupMap[groupKey] = validatorGroup
}

validatorGroup.Validators++

statusStr := validator.Status.String()
if strings.HasPrefix(statusStr, "active_") {
validatorGroup.Activated++

if activityMap[uint64(vIdx)] > 0 {
validatorGroup.Online++
} else {
validatorGroup.Offline++
}
}
if strings.HasPrefix(statusStr, "exited_") || strings.HasPrefix(statusStr, "withdrawal_") {
validatorGroup.Exited++
}
if strings.HasSuffix(statusStr, "_slashed") {
validatorGroup.Slashed++
}
}

// sort / filter groups
validatorGroups := maps.Values(validatorGroupMap)
switch sortOrder {
case "group":
sort.Slice(validatorGroups, func(a, b int) bool {
return strings.Compare(validatorGroups[a].GroupLower, validatorGroups[b].GroupLower) < 0
})
pageData.IsDefaultSorting = true
case "group-d":
sort.Slice(validatorGroups, func(a, b int) bool {
return strings.Compare(validatorGroups[a].GroupLower, validatorGroups[b].GroupLower) > 0
})
case "count":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Validators < validatorGroups[b].Validators
})
case "count-d":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Validators > validatorGroups[b].Validators
})
case "active":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Activated < validatorGroups[b].Activated
})
case "active-d":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Activated > validatorGroups[b].Activated
})
case "online":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Online < validatorGroups[b].Online
})
case "online-d":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Online > validatorGroups[b].Online
})
case "offline":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Offline < validatorGroups[b].Offline
})
case "offline-d":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Offline > validatorGroups[b].Offline
})
case "exited":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Exited < validatorGroups[b].Exited
})
case "exited-d":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Exited > validatorGroups[b].Exited
})
case "slashed":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Slashed < validatorGroups[b].Slashed
})
case "slashed-d":
sort.Slice(validatorGroups, func(a, b int) bool {
return validatorGroups[a].Slashed > validatorGroups[b].Slashed
})
}

groupCount := uint64(len(validatorGroups))

startIdx := pageIdx * pageSize
endIdx := startIdx + pageSize
if startIdx >= groupCount {
validatorGroups = []*models.ValidatorsActiviyPageDataGroup{}
} else if endIdx > groupCount {
validatorGroups = validatorGroups[startIdx:]
} else {
validatorGroups = validatorGroups[startIdx:endIdx]
}
pageData.Groups = validatorGroups
pageData.GroupCount = uint64(len(validatorGroups))

pageData.TotalPages = groupCount / pageSize
if groupCount%pageSize != 0 {
pageData.TotalPages++
}
pageData.LastPageIndex = pageData.TotalPages - 1
pageData.FirstGroup = startIdx
pageData.LastGroup = endIdx

if endIdx <= groupCount {
pageData.NextPageIndex = pageIdx + 1
}

sortingArg := ""
if sortOrder != "group" {
sortingArg = fmt.Sprintf("&o=%v", sortOrder)
}

pageData.ViewPageLink = fmt.Sprintf("/validators/activity?%v&c=%v", filterArgs.Encode(), pageData.PageSize)
pageData.FirstPageLink = fmt.Sprintf("/validators/activity?%v%v&c=%v", filterArgs.Encode(), sortingArg, pageData.PageSize)
pageData.PrevPageLink = fmt.Sprintf("/validators/activity?%v%v&c=%v&s=%v", filterArgs.Encode(), sortingArg, pageData.PageSize, pageData.PrevPageIndex)
pageData.NextPageLink = fmt.Sprintf("/validators/activity?%v%v&c=%v&s=%v", filterArgs.Encode(), sortingArg, pageData.PageSize, pageData.NextPageIndex)
pageData.LastPageLink = fmt.Sprintf("/validators/activity?%v%v&c=%v&s=%v", filterArgs.Encode(), sortingArg, pageData.PageSize, pageData.LastPageIndex)

return pageData
}
4 changes: 4 additions & 0 deletions services/beaconservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ func (bs *BeaconService) GetValidatorName(index uint64) string {
return bs.validatorNames.GetValidatorName(index)
}

func (bs *BeaconService) GetValidatorNamesCount() uint64 {
return bs.validatorNames.GetValidatorNamesCount()
}

func (bs *BeaconService) GetCachedValidatorSet() map[phase0.ValidatorIndex]*v1.Validator {
return bs.indexer.GetCachedValidatorSet()
}
Expand Down
12 changes: 12 additions & 0 deletions services/validatornames.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/pk910/dora/dbtypes"
"github.com/pk910/dora/utils"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"gopkg.in/yaml.v3"
)

Expand All @@ -40,6 +41,17 @@ func (vn *ValidatorNames) GetValidatorName(index uint64) string {
return vn.names[index]
}

func (vn *ValidatorNames) GetValidatorNamesCount() uint64 {
if !vn.namesMutex.TryRLock() {
return 0
}
defer vn.namesMutex.RUnlock()
if vn.names == nil {
return 0
}
return uint64(len(maps.Keys(vn.names)))
}

func (vn *ValidatorNames) LoadValidatorNames() {
vn.loadingMutex.Lock()
defer vn.loadingMutex.Unlock()
Expand Down
Loading

0 comments on commit b24339a

Please sign in to comment.