Skip to content

Commit

Permalink
added slots by validator page (/validator/{index}/slots)
Browse files Browse the repository at this point in the history
  • Loading branch information
pk910 committed Aug 7, 2023
1 parent e25c127 commit 15d5fb7
Show file tree
Hide file tree
Showing 5 changed files with 348 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func startFrontend() {
router.HandleFunc("/search/{type}", handlers.SearchAhead).Methods("GET")
router.HandleFunc("/validators", handlers.Validators).Methods("GET")
router.HandleFunc("/validator/{idxOrPubKey}", handlers.Validator).Methods("GET")
router.HandleFunc("/validator/{index}/slots", handlers.ValidatorSlots).Methods("GET")

if utils.Config.Frontend.Debug {
// serve files from local directory when debugging, instead of from go embed file
Expand Down
147 changes: 147 additions & 0 deletions handlers/validator_slots.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package handlers

import (
"fmt"
"net/http"
"strconv"
"time"

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

"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"
)

// Slots will return the main "slots" page using a go template
func ValidatorSlots(w http.ResponseWriter, r *http.Request) {
var slotsTemplateFiles = append(layoutTemplateFiles,
"validator_slots/slots.html",
"_svg/professor.html",
)

var pageTemplate = templates.GetTemplate(slotsTemplateFiles...)
vars := mux.Vars(r)
validator, _ := strconv.ParseUint(vars["index"], 10, 64)

w.Header().Set("Content-Type", "text/html")
data := InitPageData(w, r, "blockchain", fmt.Sprintf("/validators/%v/slots", validator), "Validator Slots", slotsTemplateFiles)

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

var pageData *models.ValidatorSlotsPageData

var pageIdx uint64 = 0
if urlArgs.Has("s") {
pageIdx, _ = strconv.ParseUint(urlArgs.Get("s"), 10, 64)
}
pageData = getValidatorSlotsPageData(validator, pageIdx, pageSize)

data.Data = pageData

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

func getValidatorSlotsPageData(validator uint64, pageIdx uint64, pageSize uint64) *models.ValidatorSlotsPageData {
pageData := &models.ValidatorSlotsPageData{}
pageCacheKey := fmt.Sprintf("valslots:%v:%v:%v", validator, pageIdx, pageSize)
pageData = services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(pageCall *services.FrontendCacheProcessingPage) interface{} {
pageData, cacheTimeout := buildValidatorSlotsPageData(validator, pageIdx, pageSize)
pageCall.CacheTimeout = cacheTimeout
return pageData
}).(*models.ValidatorSlotsPageData)
return pageData
}

func buildValidatorSlotsPageData(validator uint64, pageIdx uint64, pageSize uint64) (*models.ValidatorSlotsPageData, time.Duration) {
pageData := &models.ValidatorSlotsPageData{
Index: validator,
Name: services.GlobalBeaconService.GetValidatorName(validator),
}
logrus.Printf("validator slots page called (%v): %v:%v", validator, pageIdx, pageSize)
if pageIdx == 0 {
pageData.IsDefaultPage = true
}

if pageSize > 100 {
pageSize = 100
}
pageData.PageSize = pageSize
pageData.TotalPages = pageIdx + 1
pageData.CurrentPageIndex = pageIdx + 1
pageData.CurrentPageSlot = pageIdx
if pageIdx >= 1 {
pageData.PrevPageIndex = pageIdx
pageData.PrevPageSlot = pageIdx - 1
}
pageData.LastPageSlot = 0

finalizedHead, _ := services.GlobalBeaconService.GetFinalizedBlockHead()

// load slots
pageData.Slots = make([]*models.ValidatorSlotsPageDataSlot, 0)
dbBlocks := services.GlobalBeaconService.GetDbBlocksByProposer(validator, pageIdx, uint32(pageSize), true, true)
haveMore := false
for idx, blockAssignment := range dbBlocks {
if idx >= int(pageSize) {
haveMore = true
break
}
slot := blockAssignment.Slot
finalized := false
if finalizedHead != nil && uint64(finalizedHead.Data.Header.Message.Slot) >= slot {
finalized = true
}
blockStatus := uint8(0)

slotData := &models.ValidatorSlotsPageDataSlot{
Slot: slot,
Epoch: utils.EpochOfSlot(slot),
Ts: utils.SlotToTime(slot),
Finalized: finalized,
Status: blockStatus,
Proposer: validator,
ProposerName: pageData.Name,
}

if blockAssignment.Block != nil {
dbBlock := blockAssignment.Block
if dbBlock.Orphaned {
slotData.Status = 2
} else {
slotData.Status = 1
}
slotData.AttestationCount = dbBlock.AttestationCount
slotData.DepositCount = dbBlock.DepositCount
slotData.ExitCount = dbBlock.ExitCount
slotData.ProposerSlashingCount = dbBlock.ProposerSlashingCount
slotData.AttesterSlashingCount = dbBlock.AttesterSlashingCount
slotData.SyncParticipation = float64(dbBlock.SyncParticipation) * 100
slotData.EthTransactionCount = dbBlock.EthTransactionCount
slotData.EthBlockNumber = dbBlock.EthBlockNumber
slotData.Graffiti = dbBlock.Graffiti
slotData.BlockRoot = dbBlock.Root
}
pageData.Slots = append(pageData.Slots, slotData)
}
pageData.SlotCount = uint64(len(pageData.Slots))
if pageData.SlotCount > 0 {
pageData.FirstSlot = pageData.Slots[0].Slot
pageData.LastSlot = pageData.Slots[pageData.SlotCount-1].Slot
}
if haveMore {
pageData.NextPageIndex = pageIdx + 1
pageData.NextPageSlot = pageIdx + 1
pageData.TotalPages++
}

return pageData, 5 * time.Minute
}
1 change: 1 addition & 0 deletions templates/validator/recentBlocks.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<div class="card-header">
<h4 class="card-title d-flex justify-content-between align-items-center" style="margin: .5rem 0;">
<span><i class="fa fa-cubes"></i> Most recent blocks</span>
<a class="btn btn-primary btn-sm float-right text-white" href="/validator/{{ .Index }}/slots">View more</a>
</h4>
</div>
<div class="card-body p-0">
Expand Down
150 changes: 150 additions & 0 deletions templates/validator_slots/slots.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
{{ define "page" }}
<div class="container mt-2">
<div class="d-md-flex py-2 justify-content-md-between">
<h1 class="h4 mb-1 mb-md-0"><i class="fas fa-cube mx-2"></i> Validator {{ formatValidatorWithIndex .Index .Name }}: Slots</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb font-size-1 mb-0" style="padding:0; background-color:transparent;">
<li class="breadcrumb-item"><a href="/" title="Home">Home</a></li>
<li class="breadcrumb-item"><a href="/validators" title="Validators">Validators</a></li>
<li class="breadcrumb-item"><a href="/validator/{{ .Index }}" title="Validator {{ .Index }}">{{ .Index }}</a></li>
<li class="breadcrumb-item active" aria-current="page">Slots</li>
</ol>
</nav>
</div>

<div class="card mt-2">
<div id="header-placeholder" style="height:45px;"></div>
<div class="card-body px-0 py-3">
<div class="row">
<div class="col-sm-12 col-md-6 table-pagesize">
<form action="/validator/{{ .Index }}/slots" method="get">
<label class="px-2">
<span>Show </span>
<select name="c" aria-controls="slots" class="custom-select custom-select-sm form-control form-control-sm" onchange="this.form.submit()">
<option value="{{ .PageSize }}" selected>{{ .PageSize }}</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
{{ if not .IsDefaultPage }}
<input name="s" type="hidden" value="{{ .CurrentPageSlot }}">
{{ end }}
<span> entries</span>
</label>
</form>
</div>
</div>
<div class="table-responsive px-0 py-1">
<table class="table table-nobr" id="slots">
<thead>
<tr>
<th>Epoch</th>
<th>Slot</th>
<th>Status</th>
<th style="min-width: 125px">Time</th>
<th>Prop<span class="d-none d-lg-inline">oser</span></th>
<th class="d-none d-md-table-cell">Att<span class="d-none d-lg-inline">estations</span></th>
<th>
<nobr><span data-toggle="tooltip" data-placement="top" title="Deposits">D<span class="d-none d-lg-inline">eposits</span> </span> /
<span data-toggle="tooltip" data-placement="top" title="Exits">E<span class="d-none d-lg-inline">xits</span> </span></nobr>
</th>
<th><span class="d-none d-lg-inline">Slashings</span>
<nobr><span data-toggle="tooltip" data-placement="top" title="Proposer Slashings">P</span> /
<span data-toggle="tooltip" data-placement="top" title="Attester Slashings">A</span></nobr>
</th>
<th>Tx<span class="d-none d-lg-inline"> Count</span></th>
<th>Sync<span class="d-none d-lg-inline"> Agg</span> %</th>
<th>Graffiti</th>
</tr>
</thead>
{{ if gt .SlotCount 0 }}
<tbody>
{{ range $i, $slot := .Slots }}
<tr>
<td><a href="/epoch/{{ $slot.Epoch }}">{{ formatAddCommas $slot.Epoch }}</a></td>
{{ if eq $slot.Status 2 }}
<td><a href="/slot/0x{{ printf "%x" $slot.BlockRoot }}">{{ formatAddCommas $slot.Slot }}</a></td>
{{ else }}
<td><a href="/slot/{{ $slot.Slot }}">{{ formatAddCommas $slot.Slot }}</a></td>
{{ end }}
<td>
{{ if eq $slot.Slot 0 }}
<span class="badge rounded-pill text-bg-info">Genesis</span>
{{ else if $slot.Scheduled }}
<span class="badge rounded-pill text-bg-secondary">Scheduled</span>
{{ else if eq $slot.Status 0 }}
<span class="badge rounded-pill text-bg-warning">Missed</span>
{{ else if eq $slot.Status 1 }}
<span class="badge rounded-pill text-bg-success">Proposed</span>
{{ else if eq $slot.Status 2 }}
<span class="badge rounded-pill text-bg-info">Orphaned</span>
{{ else }}
<span class="badge rounded-pill text-bg-dark">Unknown</span>
{{ end }}
</td>
<td data-timer="{{ $slot.Ts.Unix }}"><span data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ $slot.Ts }}">{{ formatRecentTimeShort $slot.Ts }}</span></td>
<td>{{ formatValidator $slot.Proposer $slot.ProposerName }}</td>
<td class="d-none d-md-table-cell">{{ if not (eq $slot.Status 0) }}{{ $slot.AttestationCount }}{{ end }}</td>
<td>{{ if not (eq $slot.Status 0) }}{{ $slot.DepositCount }} / {{ $slot.ExitCount }}{{ end }}</td>
<td>{{ if not (eq $slot.Status 0) }}{{ $slot.ProposerSlashingCount }} / {{ $slot.AttesterSlashingCount }}{{ end }}</td>
<td>{{ if not (eq $slot.Status 0) }}{{ $slot.EthTransactionCount }}{{ end }}</td>
<td>{{ if not (eq $slot.Status 0) }}{{ formatFloat $slot.SyncParticipation 2 }}%{{ end }}</td>
<td>{{ if not (eq $slot.Status 0) }}{{ formatGraffiti $slot.Graffiti }}{{ end }}</td>
</tr>
{{ end }}
</tbody>
{{ else }}
<tbody>
<tr style="height: 430px;">
<td class="d-none d-md-table-cell"></td>
<td style="vertical-align: middle;" colspan="9">
<div class="img-fluid mx-auto p-3 d-flex align-items-center" style="max-height: 400px; max-width: 400px; overflow: hidden;">
{{ template "professor_svg" }}
</div>
</td>
<td class="d-none d-md-table-cell"></td>
</tr>
</tbody>
{{ end }}
</table>
</div>
{{ if gt .TotalPages 1 }}
<div class="row">
<div class="col-sm-12 col-md-5 table-metainfo">
<div class="px-2">
<div class="table-meta" role="status" aria-live="polite">Showing slot {{ .FirstSlot }} to {{ .LastSlot }}</div>
</div>
</div>
<div class="col-sm-12 col-md-7 table-paging">
<div class="d-inline-block px-2">
<ul class="pagination">
<li class="first paginate_button page-item {{ if le .PrevPageIndex 1 }}disabled{{ end }}" id="tpg_first">
<a tab-index="1" aria-controls="tpg_first" class="page-link" href="/validator/{{ .Index }}/slots?c={{ .PageSize }}">First</a>
</li>
<li class="previous paginate_button page-item {{ if eq .PrevPageIndex 0 }}disabled{{ end }}" id="tpg_previous">
<a tab-index="1" aria-controls="tpg_previous" class="page-link" href="/validator/{{ .Index }}/slots?s={{ .PrevPageSlot }}&c={{ .PageSize }}"><i class="fas fa-chevron-left"></i></a>
</li>
<li class="page-item disabled">
<a class="page-link" style="background-color: transparent;">{{ .CurrentPageIndex }} of {{ .TotalPages }}</a>
</li>
<li class="next paginate_button page-item {{ if eq .NextPageIndex 0 }}disabled{{ end }}" id="tpg_next">
<a tab-index="1" aria-controls="tpg_next" class="page-link" href="/validator/{{ .Index }}/slots?s={{ .NextPageSlot }}&c={{ .PageSize }}"><i class="fas fa-chevron-right"></i></a>
</li>
<li class="last paginate_button page-item {{ if or (eq .LastPageSlot 0) (le .NextPageSlot .LastPageSlot) }}disabled{{ end }}" id="tpg_last">
<a tab-index="1" aria-controls="tpg_last" class="page-link" href="/validator/{{ .Index }}/slots?s={{ .LastPageSlot }}&c={{ .PageSize }}">Last</a>
</li>
</ul>
</div>
</div>
</div>
{{ end }}
</div>
<div id="footer-placeholder" style="height:71px;"></div>
</div>
</div>
{{ end }}
{{ define "js" }}
{{ end }}
{{ define "css" }}
{{ end }}
49 changes: 49 additions & 0 deletions types/models/validator_slots.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package models

import (
"time"
)

// SlotsPageData is a struct to hold info for the slots page
type ValidatorSlotsPageData struct {
Index uint64 `json:"index"`
Name string `json:"name"`

Slots []*ValidatorSlotsPageDataSlot `json:"slots"`
SlotCount uint64 `json:"slot_count"`
FirstSlot uint64 `json:"first_slot"`
LastSlot uint64 `json:"last_slot"`
GraffitiFilter string `json:"graffiti_filter"`

IsDefaultPage bool `json:"default_page"`
TotalPages uint64 `json:"total_pages"`
PageSize uint64 `json:"page_size"`
CurrentPageIndex uint64 `json:"page_index"`
CurrentPageSlot uint64 `json:"page_slot"`
PrevPageIndex uint64 `json:"prev_page_index"`
PrevPageSlot uint64 `json:"prev_page_slot"`
NextPageIndex uint64 `json:"next_page_index"`
NextPageSlot uint64 `json:"next_page_slot"`
LastPageSlot uint64 `json:"last_page_slot"`
}

type ValidatorSlotsPageDataSlot struct {
Slot uint64 `json:"slot"`
Epoch uint64 `json:"epoch"`
Ts time.Time `json:"ts"`
Finalized bool `json:"scheduled"`
Scheduled bool `json:"finalized"`
Status uint8 `json:"status"`
Proposer uint64 `json:"proposer"`
ProposerName string `json:"proposer_name"`
AttestationCount uint64 `json:"attestation_count"`
DepositCount uint64 `json:"deposit_count"`
ExitCount uint64 `json:"exit_count"`
ProposerSlashingCount uint64 `json:"proposer_slashing_count"`
AttesterSlashingCount uint64 `json:"attester_slashing_count"`
SyncParticipation float64 `json:"sync_participation"`
EthTransactionCount uint64 `json:"eth_transaction_count"`
EthBlockNumber uint64 `json:"eth_block_number"`
Graffiti []byte `json:"graffiti"`
BlockRoot []byte `json:"block_root"`
}

0 comments on commit 15d5fb7

Please sign in to comment.