Skip to content

Commit

Permalink
Merge pull request #3 from pk910/validator-details
Browse files Browse the repository at this point in the history
Add validator overview & details
  • Loading branch information
pk910 authored Jul 31, 2023
2 parents 865bf95 + bc992c7 commit 9e76608
Show file tree
Hide file tree
Showing 26 changed files with 1,420 additions and 18 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ Things that might be worth adding at some time

* Validator Overview & Details\
The current validator set is actually already maintained in memory. So it should be easy to add pages for basic validator related stuff.
* [ ] Page: Validators List (`/validators`)
* [ ] Page: Validator Details (`/validator/{validator_index}`)
* [ ] Rough overview with status (activated, slashed, ...) & current balance
* [ ] Recent Blocks (from db)
* [x] Page: Validators List (`/validators`)
* [x] Page: Validator Details (`/validator/{validator_index}`)
* [x] Rough overview with status (activated, slashed, ...) & current balance
* [x] Recent Blocks (from db)
* [ ] Recent Attestations (from cache)
* Track Sync Committees
* [ ] Database: table sync_committees (Sync Committee index)
Expand Down
2 changes: 2 additions & 0 deletions cmd/explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ func startFrontend() {
router.HandleFunc("/slot/{slotOrHash}", handlers.Slot).Methods("GET")
router.HandleFunc("/search", handlers.Search).Methods("GET")
router.HandleFunc("/search/{type}", handlers.SearchAhead).Methods("GET")
router.HandleFunc("/validators", handlers.Validators).Methods("GET")
router.HandleFunc("/validator/{idxOrPubKey}", handlers.Validator).Methods("GET")

if utils.Config.Frontend.Debug {
// serve files from local directory when debugging, instead of from go embed file
Expand Down
5 changes: 2 additions & 3 deletions config/default.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
chain:
name: "mainnet"
#genesisTimestamp: 1688126460
#genesisValidatorsRoot: "0xbf3c3d4683a5a4d286cd2a5ef7a5c1702f649eee82cdc7e87e05030102d12ccf"
#configPath: "../ephemery/config.yaml"
#displayName: "Ephemery Iteration xy"

Expand Down Expand Up @@ -31,10 +30,10 @@ beaconapi:
# CL Client RPC
endpoint: "http://127.0.0.1:5052"

# local cache for RPC calls
# local cache for page models
localCacheSize: 100 # 100MB

# remote cache for RPC calls & page models
# remote cache for page models
redisCacheAddr: ""
redisCachePrefix: ""

Expand Down
68 changes: 68 additions & 0 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/jackc/pgx/v4/pgxpool"
_ "github.com/jackc/pgx/v4/stdlib"
"github.com/mitchellh/mapstructure"
)

//go:embed migrations/*.sql
Expand Down Expand Up @@ -358,6 +359,73 @@ func GetBlocksWithGraffiti(graffiti string, firstSlot uint64, offset uint64, lim
return blocks
}

func GetAssignedBlocks(proposer uint64, firstSlot uint64, offset uint64, limit uint32, withOrphaned bool) []*dbtypes.AssignedBlock {
blockAssignments := []*dbtypes.AssignedBlock{}
orphanedLimit := ""
if !withOrphaned {
orphanedLimit = "AND NOT orphaned"
}
var sql strings.Builder
fmt.Fprintf(&sql, `SELECT slot_assignments.slot, slot_assignments.proposer`)
blockFields := []string{
"root", "slot", "parent_root", "state_root", "orphaned", "proposer", "graffiti", "graffiti_text",
"attestation_count", "deposit_count", "exit_count", "withdraw_count", "withdraw_amount", "attester_slashing_count",
"proposer_slashing_count", "bls_change_count", "eth_transaction_count", "eth_block_number", "eth_block_hash", "sync_participation",
}
for _, blockField := range blockFields {
fmt.Fprintf(&sql, ", blocks.%v AS \"block.%v\"", blockField, blockField)
}
fmt.Fprintf(&sql, `
FROM slot_assignments
LEFT JOIN blocks ON blocks.slot = slot_assignments.slot
WHERE slot_assignments.proposer = $1 AND slot_assignments.slot < $2 `+orphanedLimit+`
ORDER BY slot_assignments.slot DESC
LIMIT $3 OFFSET $4
`)
rows, err := ReaderDb.Query(sql.String(), proposer, firstSlot, limit, offset)
if err != nil {
logger.Errorf("Error while fetching blocks: %v", err)
return nil
}

scanArgs := make([]interface{}, len(blockFields)+2)
for rows.Next() {
scanVals := make([]interface{}, len(blockFields)+2)
for i := range scanArgs {
scanArgs[i] = &scanVals[i]
}
err := rows.Scan(scanArgs...)
if err != nil {
logger.Errorf("Error while parsing block: %v", err)
continue
}

blockAssignment := dbtypes.AssignedBlock{}
blockAssignment.Slot = uint64(scanVals[0].(int64))
blockAssignment.Proposer = uint64(scanVals[1].(int64))

if scanVals[2] != nil {
blockValMap := map[string]interface{}{}
for idx, fName := range blockFields {
blockValMap[fName] = scanVals[idx+2]
}
var block dbtypes.Block
cfg := &mapstructure.DecoderConfig{
Metadata: nil,
Result: &block,
TagName: "db",
}
decoder, _ := mapstructure.NewDecoder(cfg)
decoder.Decode(blockValMap)
blockAssignment.Block = &block
}

blockAssignments = append(blockAssignments, &blockAssignment)
}

return blockAssignments
}

func GetSlotAssignmentsForSlots(firstSlot uint64, lastSlot uint64) []*dbtypes.SlotAssignment {
assignments := []*dbtypes.SlotAssignment{}
err := ReaderDb.Select(&assignments, `
Expand Down
7 changes: 7 additions & 0 deletions dbtypes/other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dbtypes

type AssignedBlock struct {
Slot uint64 `db:"slot"`
Proposer uint64 `db:"proposer"`
Block *Block `db:"block"`
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ require (
gopkg.in/yaml.v3 v3.0.1
)

require github.com/mitchellh/mapstructure v1.5.0 // indirect

require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
Expand Down
9 changes: 9 additions & 0 deletions handlers/pageData.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ func createMenuItems(active string, isMain bool) []types.MainMenuItem {
},
},
},
{
Links: []types.NavigationLink{
{
Label: "Validators",
Path: "/validators",
Icon: "fa-table",
},
},
},
},
},
}
Expand Down
174 changes: 174 additions & 0 deletions handlers/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package handlers

import (
"bytes"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/gorilla/mux"
"github.com/sirupsen/logrus"

"github.com/pk910/light-beaconchain-explorer/rpctypes"
"github.com/pk910/light-beaconchain-explorer/services"
"github.com/pk910/light-beaconchain-explorer/templates"
"github.com/pk910/light-beaconchain-explorer/types/models"
"github.com/pk910/light-beaconchain-explorer/utils"
)

// Validator will return the main "validator" page using a go template
func Validator(w http.ResponseWriter, r *http.Request) {
var validatorTemplateFiles = append(layoutTemplateFiles,
"validator/validator.html",
"validator/recentBlocks.html",
"_svg/timeline.html",
)
var notfoundTemplateFiles = append(layoutTemplateFiles,
"validator/notfound.html",
)

var pageTemplate = templates.GetTemplate(validatorTemplateFiles...)

w.Header().Set("Content-Type", "text/html")
data := InitPageData(w, r, "validators", "/validator", "Validator", validatorTemplateFiles)

validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet()
var validator *rpctypes.ValidatorEntry
if validatorSetRsp != nil {
vars := mux.Vars(r)
idxOrPubKey := strings.Replace(vars["idxOrPubKey"], "0x", "", -1)
validatorPubKey, err := hex.DecodeString(idxOrPubKey)
if err != nil || len(validatorPubKey) != 48 {
// search by index^
validatorIndex, err := strconv.ParseUint(vars["idxOrPubKey"], 10, 64)
if err == nil && validatorIndex < uint64(len(validatorSetRsp.Data)) {
validator = &validatorSetRsp.Data[validatorIndex]
}
} else {
// search by pubkey
for _, val := range validatorSetRsp.Data {
if bytes.Equal(val.Validator.PubKey, validatorPubKey) {
validator = &val
break
}
}
}
}

if validator == nil {
data := InitPageData(w, r, "blockchain", "/validator", "Validator not found", notfoundTemplateFiles)
if handleTemplateError(w, r, "validator.go", "Validator", "", templates.GetTemplate(notfoundTemplateFiles...).ExecuteTemplate(w, "layout", data)) != nil {
return // an error has occurred and was processed
}
return
}

data.Data = getValidatorPageData(uint64(validator.Index))

if handleTemplateError(w, r, "validators.go", "Validators", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil {
return // an error has occurred and was processed
}
}

func getValidatorPageData(validatorIndex uint64) *models.ValidatorPageData {
pageData := &models.ValidatorPageData{}
pageCacheKey := fmt.Sprintf("validator:%v", validatorIndex)
if !utils.Config.Frontend.Debug && services.GlobalBeaconService.GetFrontendCache(pageCacheKey, pageData) == nil {
logrus.Printf("validator page served from cache: %v", validatorIndex)
return pageData
}
logrus.Printf("validator page called: %v", validatorIndex)

validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet()
validator := validatorSetRsp.Data[validatorIndex]

pageData = &models.ValidatorPageData{
CurrentEpoch: uint64(utils.TimeToEpoch(time.Now())),
Index: uint64(validator.Index),
Name: services.GlobalBeaconService.GetValidatorName(uint64(validator.Index)),
PublicKey: validator.Validator.PubKey,
Balance: uint64(validator.Balance),
EffectiveBalance: uint64(validator.Validator.EffectiveBalance),
BeaconState: validator.Status,
WithdrawCredentials: validator.Validator.WithdrawalCredentials,
}
if strings.HasPrefix(validator.Status, "pending") {
pageData.State = "Pending"
} else if validator.Status == "active_ongoing" {
pageData.State = "Active"
pageData.IsActive = true
} else if validator.Status == "active_exiting" {
pageData.State = "Exiting"
pageData.IsActive = true
} else if validator.Status == "active_slashed" {
pageData.State = "Slashed"
pageData.IsActive = true
} else if validator.Status == "exited_unslashed" {
pageData.State = "Exited"
} else if validator.Status == "exited_slashed" {
pageData.State = "Slashed"
} else {
pageData.State = validator.Status
}

if pageData.IsActive {
// load activity map
activityMap, maxActivity := services.GlobalBeaconService.GetValidatorActivity()
pageData.UpcheckActivity = activityMap[uint64(validator.Index)]
pageData.UpcheckMaximum = uint8(maxActivity)
}

if validator.Validator.ActivationEligibilityEpoch < 18446744073709551615 {
pageData.ShowEligible = true
pageData.EligibleEpoch = uint64(validator.Validator.ActivationEligibilityEpoch)
pageData.EligibleTs = utils.EpochToTime(uint64(validator.Validator.ActivationEligibilityEpoch))
}
if validator.Validator.ActivationEpoch < 18446744073709551615 {
pageData.ShowActivation = true
pageData.ActivationEpoch = uint64(validator.Validator.ActivationEpoch)
pageData.ActivationTs = utils.EpochToTime(uint64(validator.Validator.ActivationEpoch))
}
if validator.Validator.ExitEpoch < 18446744073709551615 {
pageData.ShowExit = true
pageData.WasActive = true
pageData.ExitEpoch = uint64(validator.Validator.ExitEpoch)
pageData.ExitTs = utils.EpochToTime(uint64(validator.Validator.ExitEpoch))
}
if validator.Validator.WithdrawalCredentials[0] == 0x01 {
pageData.ShowWithdrawAddress = true
pageData.WithdrawAddress = validator.Validator.WithdrawalCredentials[12:]
}

// load latest blocks
pageData.RecentBlocks = make([]*models.ValidatorPageDataBlocks, 0)
blocksData := services.GlobalBeaconService.GetDbBlocksByProposer(validatorIndex, 0, 10, true, true)
for _, blockData := range blocksData {
blockStatus := 1
if blockData.Block == nil {
blockStatus = 0
} else if blockData.Block.Orphaned {
blockStatus = 2
}
blockEntry := models.ValidatorPageDataBlocks{
Epoch: utils.EpochOfSlot(blockData.Slot),
Slot: blockData.Slot,
Ts: utils.SlotToTime(blockData.Slot),
Status: uint64(blockStatus),
}
if blockData.Block != nil {
blockEntry.Graffiti = blockData.Block.Graffiti
blockEntry.EthBlock = blockData.Block.EthBlockNumber
blockEntry.BlockRoot = fmt.Sprintf("0x%x", blockData.Block.Root)
}
pageData.RecentBlocks = append(pageData.RecentBlocks, &blockEntry)
}
pageData.RecentBlockCount = uint64(len(pageData.RecentBlocks))

if pageCacheKey != "" {
services.GlobalBeaconService.SetFrontendCache(pageCacheKey, pageData, 10*time.Minute)
}
return pageData
}
Loading

0 comments on commit 9e76608

Please sign in to comment.