From 53618bf9252a3a5bf936a8e0880c78d745878548 Mon Sep 17 00:00:00 2001 From: pk910 Date: Thu, 24 Aug 2023 09:35:33 +0200 Subject: [PATCH] Added chain visualization (fork graph) & clients page (#9) * added fork graph to slots page * added `/clients` page * cleanup forkgraph.css --- cmd/explorer/main.go | 1 + db/db.go | 18 ++++ db/schema/pgsql/20230720234842_init.sql | 4 + db/schema/sqlite/20230720234842_init.sql | 4 + handlers/clients.go | 68 ++++++++++++ handlers/pageData.go | 9 ++ handlers/slots.go | 131 ++++++++++++++++++++++- indexer/client.go | 24 ++++- indexer/indexer.go | 20 +++- rpc/beaconapi.go | 2 +- services/beaconservice.go | 17 +++ static/css/forkgraph.css | 67 ++++++++++++ templates/clients/clients.html | 59 ++++++++++ templates/slots/slots.html | 22 ++++ types/models/clients.go | 16 +++ types/models/slots.go | 49 +++++---- 16 files changed, 487 insertions(+), 24 deletions(-) create mode 100644 handlers/clients.go create mode 100644 static/css/forkgraph.css create mode 100644 templates/clients/clients.html create mode 100644 types/models/clients.go diff --git a/cmd/explorer/main.go b/cmd/explorer/main.go index cd395d6d..5395d88d 100644 --- a/cmd/explorer/main.go +++ b/cmd/explorer/main.go @@ -68,6 +68,7 @@ func startFrontend() { router := mux.NewRouter() router.HandleFunc("/", handlers.Index).Methods("GET") + router.HandleFunc("/clients", handlers.Clients).Methods("GET") router.HandleFunc("/epochs", handlers.Epochs).Methods("GET") router.HandleFunc("/epoch/{epoch}", handlers.Epoch).Methods("GET") router.HandleFunc("/slots", handlers.Slots).Methods("GET") diff --git a/db/db.go b/db/db.go index ee812a61..8d43c105 100644 --- a/db/db.go +++ b/db/db.go @@ -442,6 +442,24 @@ func GetBlocksForSlots(firstSlot uint64, lastSlot uint64, withOrphaned bool) []* return blocks } +func GetBlocksByParentRoot(parentRoot []byte) []*dbtypes.Block { + blocks := []*dbtypes.Block{} + err := ReaderDb.Select(&blocks, ` + SELECT + 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 + FROM blocks + WHERE parent_root = $1 + ORDER BY slot DESC + `, parentRoot) + if err != nil { + logger.Errorf("Error while fetching blocks by parent root: %v", err) + return nil + } + return blocks +} + func GetBlocksWithGraffiti(graffiti string, firstSlot uint64, offset uint64, limit uint32, withOrphaned bool) []*dbtypes.Block { blocks := []*dbtypes.Block{} orphanedLimit := "" diff --git a/db/schema/pgsql/20230720234842_init.sql b/db/schema/pgsql/20230720234842_init.sql index 0439d1bd..c3ab0544 100644 --- a/db/schema/pgsql/20230720234842_init.sql +++ b/db/schema/pgsql/20230720234842_init.sql @@ -61,6 +61,10 @@ CREATE INDEX IF NOT EXISTS "blocks_proposer_idx" ON public."blocks" ("proposer" ASC NULLS LAST); +CREATE INDEX IF NOT EXISTS "blocks_parent_root_idx" + ON public."blocks" + ("parent_root" ASC NULLS LAST); + CREATE TABLE IF NOT EXISTS public."orphaned_blocks" ( "root" bytea NOT NULL, diff --git a/db/schema/sqlite/20230720234842_init.sql b/db/schema/sqlite/20230720234842_init.sql index 2056e293..30c8cbeb 100644 --- a/db/schema/sqlite/20230720234842_init.sql +++ b/db/schema/sqlite/20230720234842_init.sql @@ -58,6 +58,10 @@ CREATE INDEX IF NOT EXISTS "blocks_proposer_idx" ON "blocks" ("proposer" ASC); +CREATE INDEX IF NOT EXISTS "blocks_parent_root_idx" + ON "blocks" + ("parent_root" ASC); + CREATE TABLE IF NOT EXISTS "orphaned_blocks" ( "root" BLOB NOT NULL UNIQUE, diff --git a/handlers/clients.go b/handlers/clients.go new file mode 100644 index 00000000..29abe634 --- /dev/null +++ b/handlers/clients.go @@ -0,0 +1,68 @@ +package handlers + +import ( + "net/http" + "time" + + "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" + "github.com/sirupsen/logrus" +) + +// Clients will return the main "clients" page using a go template +func Clients(w http.ResponseWriter, r *http.Request) { + var clientsTemplateFiles = append(layoutTemplateFiles, + "clients/clients.html", + ) + + var pageTemplate = templates.GetTemplate(clientsTemplateFiles...) + + w.Header().Set("Content-Type", "text/html") + data := InitPageData(w, r, "clients", "/clients", "Clients", clientsTemplateFiles) + + data.Data = getClientsPageData() + + if handleTemplateError(w, r, "clients.go", "Clients", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } +} + +func getClientsPageData() *models.ClientsPageData { + pageData := &models.ClientsPageData{} + pageCacheKey := "clients" + pageData = services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(pageCall *services.FrontendCacheProcessingPage) interface{} { + pageData, cacheTimeout := buildClientsPageData() + pageCall.CacheTimeout = cacheTimeout + return pageData + }).(*models.ClientsPageData) + return pageData +} + +func buildClientsPageData() (*models.ClientsPageData, time.Duration) { + logrus.Printf("clients page called") + pageData := &models.ClientsPageData{ + Clients: []*models.ClientsPageDataClient{}, + } + cacheTime := time.Duration(utils.Config.Chain.Config.SecondsPerSlot) * time.Second + + for _, client := range services.GlobalBeaconService.GetClients() { + lastHeadSlot, lastHeadRoot := client.GetLastHead() + if lastHeadSlot < 0 { + lastHeadSlot = 0 + } + resClient := &models.ClientsPageDataClient{ + Index: int(client.GetIndex()) + 1, + Name: client.GetName(), + Version: client.GetVersion(), + HeadSlot: uint64(lastHeadSlot), + HeadRoot: lastHeadRoot, + Status: client.GetStatus(), + } + pageData.Clients = append(pageData.Clients, resClient) + } + pageData.ClientCount = uint64(len(pageData.Clients)) + + return pageData, cacheTime +} diff --git a/handlers/pageData.go b/handlers/pageData.go index 8fd5d4fe..79b994e9 100644 --- a/handlers/pageData.go +++ b/handlers/pageData.go @@ -110,6 +110,15 @@ func createMenuItems(active string, isMain bool) []types.MainMenuItem { }, }, }, + { + Links: []types.NavigationLink{ + { + Label: "Clients", + Path: "/clients", + Icon: "fa-server", + }, + }, + }, }, }, } diff --git a/handlers/slots.go b/handlers/slots.go index 7ec6225e..586d76e8 100644 --- a/handlers/slots.go +++ b/handlers/slots.go @@ -1,6 +1,7 @@ package handlers import ( + "bytes" "fmt" "math" "net/http" @@ -69,7 +70,9 @@ func getSlotsPageData(firstSlot uint64, pageSize uint64) *models.SlotsPageData { func buildSlotsPageData(firstSlot uint64, pageSize uint64) (*models.SlotsPageData, time.Duration) { logrus.Printf("slots page called: %v:%v", firstSlot, pageSize) - pageData := &models.SlotsPageData{} + pageData := &models.SlotsPageData{ + ShowForkTree: true, + } now := time.Now() currentSlot := utils.TimeToSlot(uint64(now.Unix())) @@ -127,6 +130,9 @@ func buildSlotsPageData(firstSlot uint64, pageSize uint64) (*models.SlotsPageDat dbCnt := len(dbSlots) blockCount := uint64(0) allFinalized := true + isFirstPage := firstSlot >= currentSlot + openForks := map[int][]byte{} + maxOpenFork := 0 for slotIdx := int64(firstSlot); slotIdx >= int64(lastSlot); slotIdx-- { slot := uint64(slotIdx) finalized := finalizedEpoch >= int64(utils.EpochOfSlot(slot)) @@ -161,10 +167,13 @@ func buildSlotsPageData(firstSlot uint64, pageSize uint64) (*models.SlotsPageDat EthBlockNumber: dbSlot.EthBlockNumber, Graffiti: dbSlot.Graffiti, BlockRoot: dbSlot.Root, + ParentRoot: dbSlot.ParentRoot, + ForkGraph: make([]*models.SlotsPageDataForkGraph, 0), } pageData.Slots = append(pageData.Slots, slotData) blockCount++ haveBlock = true + buildSlotsPageSlotGraph(pageData, slotData, &maxOpenFork, openForks, isFirstPage) } if !haveBlock { @@ -182,11 +191,13 @@ func buildSlotsPageData(firstSlot uint64, pageSize uint64) (*models.SlotsPageDat } pageData.Slots = append(pageData.Slots, slotData) blockCount++ + buildSlotsPageSlotGraph(pageData, slotData, &maxOpenFork, openForks, isFirstPage) } } pageData.SlotCount = uint64(blockCount) pageData.FirstSlot = firstSlot pageData.LastSlot = lastSlot + pageData.ForkTreeWidth = (maxOpenFork * 20) + 20 var cacheTimeout time.Duration if allFinalized { @@ -199,6 +210,124 @@ func buildSlotsPageData(firstSlot uint64, pageSize uint64) (*models.SlotsPageDat return pageData, cacheTimeout } +func buildSlotsPageSlotGraph(pageData *models.SlotsPageData, slotData *models.SlotsPageDataSlot, maxOpenFork *int, openForks map[int][]byte, isFirstPage bool) { + // fork tree + var forkGraphIdx int = -1 + var freeForkIdx int = -1 + getForkGraph := func(slotData *models.SlotsPageDataSlot, forkIdx int) *models.SlotsPageDataForkGraph { + forkGraph := &models.SlotsPageDataForkGraph{} + graphCount := len(slotData.ForkGraph) + if graphCount > forkIdx { + forkGraph = slotData.ForkGraph[forkIdx] + } else { + for graphCount <= forkIdx { + forkGraph = &models.SlotsPageDataForkGraph{ + Index: graphCount, + Left: 10 + (graphCount * 20), + Tiles: map[string]bool{}, + } + slotData.ForkGraph = append(slotData.ForkGraph, forkGraph) + graphCount++ + } + } + return forkGraph + } + + for forkIdx := 0; forkIdx < *maxOpenFork; forkIdx++ { + forkGraph := getForkGraph(slotData, forkIdx) + if openForks[forkIdx] == nil { + if freeForkIdx == -1 { + freeForkIdx = forkIdx + } + continue + } else { + forkGraph.Tiles["vline"] = true + if bytes.Equal(openForks[forkIdx], slotData.BlockRoot) { + if forkGraphIdx != -1 { + continue + } + forkGraphIdx = forkIdx + openForks[forkIdx] = slotData.ParentRoot + forkGraph.Block = true + for targetIdx := forkIdx + 1; targetIdx < *maxOpenFork; targetIdx++ { + if openForks[targetIdx] == nil || !bytes.Equal(openForks[targetIdx], slotData.BlockRoot) { + continue + } + for idx := forkIdx + 1; idx <= targetIdx; idx++ { + splitGraph := getForkGraph(slotData, idx) + if idx == targetIdx { + splitGraph.Tiles["tline"] = true + splitGraph.Tiles["lline"] = true + splitGraph.Tiles["fork"] = true + } else { + splitGraph.Tiles["hline"] = true + } + } + forkGraph.Tiles["rline"] = true + openForks[targetIdx] = nil + } + } + } + } + if forkGraphIdx == -1 && slotData.Status > 0 { + // fork head + hasHead := false + hasForks := false + if !isFirstPage { + // get blocks that build on top of this + refBlocks := services.GlobalBeaconService.GetDbBlocksByParentRoot(slotData.BlockRoot) + refBlockCount := len(refBlocks) + if refBlockCount > 0 { + freeForkIdx = *maxOpenFork + *maxOpenFork++ + hasHead = true + + // add additional forks + if refBlockCount > 1 { + for idx := 1; idx < refBlockCount; idx++ { + graphIdx := *maxOpenFork + *maxOpenFork++ + splitGraph := getForkGraph(slotData, graphIdx) + splitGraph.Tiles["tline"] = true + splitGraph.Tiles["lline"] = true + splitGraph.Tiles["fork"] = true + if idx < refBlockCount-1 { + splitGraph.Tiles["hline"] = true + } + } + } + + // add line up to the top for each fork + for _, slot := range pageData.Slots { + if bytes.Equal(slot.BlockRoot, slotData.BlockRoot) { + continue + } + for idx := 0; idx < refBlockCount; idx++ { + splitGraph := getForkGraph(slot, freeForkIdx+idx) + splitGraph.Tiles["vline"] = true + } + } + } + } + + if freeForkIdx == -1 { + freeForkIdx = *maxOpenFork + *maxOpenFork++ + } + openForks[freeForkIdx] = slotData.ParentRoot + forkGraph := getForkGraph(slotData, freeForkIdx) + forkGraph.Block = true + if hasHead { + forkGraph.Tiles["vline"] = true + if hasForks { + forkGraph.Tiles["rline"] = true + } + } else { + forkGraph.Tiles["bline"] = true + } + } +} + func getSlotsPageDataWithGraffitiFilter(graffiti string, pageIdx uint64, pageSize uint64) *models.SlotsPageData { pageData := &models.SlotsPageData{} pageCacheKey := fmt.Sprintf("slots:%v:%v:g-%v", pageIdx, pageSize, graffiti) diff --git a/indexer/client.go b/indexer/client.go index 99db530d..d434612c 100644 --- a/indexer/client.go +++ b/indexer/client.go @@ -48,12 +48,34 @@ func newIndexerClient(clientIdx uint8, clientName string, rpcClient *rpc.BeaconC return &client } -func (client *IndexerClient) getLastHead() (int64, []byte) { +func (client *IndexerClient) GetIndex() uint8 { + return client.clientIdx +} + +func (client *IndexerClient) GetName() string { + return client.clientName +} + +func (client *IndexerClient) GetVersion() string { + return client.versionStr +} + +func (client *IndexerClient) GetLastHead() (int64, []byte) { client.cacheMutex.RLock() defer client.cacheMutex.RUnlock() return client.lastHeadSlot, client.lastHeadRoot } +func (client *IndexerClient) GetStatus() string { + if !client.isConnected { + return "disconnected" + } else if client.isSynchronizing { + return "synchronizing" + } else { + return "ready" + } +} + func (client *IndexerClient) runIndexerClientLoop() { for { err := client.runIndexerClient() diff --git a/indexer/indexer.go b/indexer/indexer.go index dccbc1af..01832729 100644 --- a/indexer/indexer.go +++ b/indexer/indexer.go @@ -51,6 +51,10 @@ func (indexer *Indexer) AddClient(index uint8, name string, endpoint string, arc return client } +func (indexer *Indexer) GetClients() []*IndexerClient { + return indexer.indexerClients +} + func (indexer *Indexer) getReadyClient(archive bool, head []byte) *IndexerClient { headCandidates := indexer.GetHeadForks() if len(headCandidates) == 0 { @@ -126,7 +130,7 @@ func (indexer *Indexer) GetHeadForks() []*HeadFork { if !client.isConnected || client.isSynchronizing { continue } - cHeadSlot, cHeadRoot := client.getLastHead() + cHeadSlot, cHeadRoot := client.GetLastHead() var matchingFork *HeadFork for _, fork := range headForks { if bytes.Equal(fork.Root, cHeadRoot) || indexer.indexerCache.isCanonicalBlock(cHeadRoot, fork.Root) { @@ -160,7 +164,7 @@ func (indexer *Indexer) GetHeadForks() []*HeadFork { }) for _, client := range fork.AllClients { var headDistance uint64 = 0 - _, cHeadRoot := client.getLastHead() + _, cHeadRoot := client.GetLastHead() if !bytes.Equal(fork.Root, cHeadRoot) { _, headDistance = indexer.indexerCache.getCanonicalDistance(cHeadRoot, fork.Root) } @@ -262,6 +266,18 @@ func (indexer *Indexer) GetCachedBlocksByProposer(proposer uint64) []*CacheBlock return resBlocks } +func (indexer *Indexer) GetCachedBlocksByParentRoot(parentRoot []byte) []*CacheBlock { + indexer.indexerCache.cacheMutex.RLock() + defer indexer.indexerCache.cacheMutex.RUnlock() + resBlocks := make([]*CacheBlock, 0) + for _, block := range indexer.indexerCache.rootMap { + if block.header != nil && bytes.Equal(block.header.Message.ParentRoot, parentRoot) { + resBlocks = append(resBlocks, block) + } + } + return resBlocks +} + func (indexer *Indexer) GetCachedEpochStats(epoch uint64) *EpochStats { _, headRoot := indexer.GetCanonicalHead() return indexer.getCachedEpochStats(epoch, headRoot) diff --git a/rpc/beaconapi.go b/rpc/beaconapi.go index 9762f385..be63f4a8 100644 --- a/rpc/beaconapi.go +++ b/rpc/beaconapi.go @@ -147,7 +147,7 @@ func (bc *BeaconClient) GetNodeSyncing() (*rpctypes.StandardV1NodeSyncingRespons func (bc *BeaconClient) GetNodeVersion() (*rpctypes.StandardV1NodeVersionResponse, error) { var parsedRsp rpctypes.StandardV1NodeVersionResponse - err := bc.getJson(fmt.Sprintf("%s/eth/v1/node/syncing", bc.endpoint), &parsedRsp) + err := bc.getJson(fmt.Sprintf("%s/eth/v1/node/version", bc.endpoint), &parsedRsp) if err != nil { if err == errNotFound { // no block found diff --git a/services/beaconservice.go b/services/beaconservice.go index eeb034c9..cd3a82df 100644 --- a/services/beaconservice.go +++ b/services/beaconservice.go @@ -64,6 +64,10 @@ func StartBeaconService() error { return nil } +func (bs *BeaconService) GetClients() []*indexer.IndexerClient { + return bs.indexer.GetClients() +} + func (bs *BeaconService) GetValidatorName(index uint64) string { return bs.validatorNames.GetValidatorName(index) } @@ -637,6 +641,19 @@ func (bs *BeaconService) GetDbBlocksByProposer(proposer uint64, pageIdx uint64, return resBlocks } +func (bs *BeaconService) GetDbBlocksByParentRoot(parentRoot []byte) []*dbtypes.Block { + parentBlock := bs.indexer.GetCachedBlock(parentRoot) + cachedMatches := bs.indexer.GetCachedBlocksByParentRoot(parentRoot) + resBlocks := make([]*dbtypes.Block, len(cachedMatches)) + for idx, block := range cachedMatches { + resBlocks[idx] = bs.indexer.BuildLiveBlock(block) + } + if parentBlock == nil { + resBlocks = append(resBlocks, db.GetBlocksByParentRoot(parentRoot)...) + } + return resBlocks +} + func (bs *BeaconService) GetValidatorActivity() (map[uint64]uint8, uint64) { activityMap := map[uint64]uint8{} epochLimit := uint64(3) diff --git a/static/css/forkgraph.css b/static/css/forkgraph.css new file mode 100644 index 00000000..c731a3e9 --- /dev/null +++ b/static/css/forkgraph.css @@ -0,0 +1,67 @@ + +.table tbody td.graph-container { + padding: 0; + position: relative; + opacity: 0.6; +} + +.graph-container .graph-fork { + position: absolute; + height: calc(100% + 1px); + width: 20px; +} + +.graph-container .graph-fork .graph-layer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: 20px; + background-position: center; + background-repeat: no-repeat repeat; +} + +.graph-container .graph-fork .graph-layer-block { + padding: 10px 4px; + font-size: 13px; + color: black; +} + +.graph-container .graph-fork .graph-layer-vline { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9wIHAYSMjc5iPMAAAAiSURBVDjLY2AgDvyHYoKAiYHKYNTAUQNHDRw1cNTAoWIgAMbwAiWGIjvFAAAAAElFTkSuQmCC"); +} + +.graph-container .graph-fork .graph-layer-bline { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9wIHAYSMjc5iPMAAAAiSURBVDjLY2AgDvyHYoKAiYHKYNTAUQNHDRw1cNTAoWIgAMbwAiWGIjvFAAAAAElFTkSuQmCC"); + top: 50%; + height: 50%; +} + +.graph-container .graph-fork .graph-layer-tline { + height: 11px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9wIHAYSMjc5iPMAAAAiSURBVDjLY2AgDvyHYoKAiYHKYNTAUQNHDRw1cNTAoWIgAMbwAiWGIjvFAAAAAElFTkSuQmCC"); +} + +.graph-container .graph-fork .graph-layer-fork { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9wIHAcELNBsanAAAACZSURBVDjLY2RgYGBkIAz+QWkmQgqZiDBMkoEEQIyBuVC6jloGakPpB9QyUANK3yDW24x4sDkDA8N/BgaGXwwMDEIE1MIwXsmNUAPnEmkYXgODoS77z8DAIEupgflIhq0iwTBGBqgmZPwbib2URMOwGvifgYHhIQMDQwsZhmF4mZuBgUGdTIMYGRgYGBmJzMtUzXqjBo4aiAYAetAczGJolWwAAAAASUVORK5CYII="); + background-repeat: no-repeat; +} + +.graph-container .graph-fork .graph-layer-hline { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9wIHAYTA3/8uYgAAAA3SURBVDjLY2AYBUMecIwGAQZgZGBg+E+RCVwMDFzfIExpBgYGJoqdxMXAwAWhGG6PRtAoGK4AAB2zAym2GI80AAAAAElFTkSuQmCC"); + background-repeat: repeat no-repeat; +} + +.graph-container .graph-fork .graph-layer-rline { + left: 50%; + width: 50%; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9wIHAYTA3/8uYgAAAA3SURBVDjLY2AYBUMecIwGAQZgZGBg+E+RCVwMDFzfIExpBgYGJoqdxMXAwAWhGG6PRtAoGK4AAB2zAym2GI80AAAAAElFTkSuQmCC"); + background-repeat: repeat no-repeat; +} + +.graph-container .graph-fork .graph-layer-lline { + width: 9px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9wIHAYTA3/8uYgAAAA3SURBVDjLY2AYBUMecIwGAQZgZGBg+E+RCVwMDFzfIExpBgYGJoqdxMXAwAWhGG6PRtAoGK4AAB2zAym2GI80AAAAAElFTkSuQmCC"); + background-repeat: repeat no-repeat; +} diff --git a/templates/clients/clients.html b/templates/clients/clients.html new file mode 100644 index 00000000..74804239 --- /dev/null +++ b/templates/clients/clients.html @@ -0,0 +1,59 @@ +{{ define "page" }} +
+
+

Clients

+ +
+ +
+
+
+ + + + + + + + + + + + + {{ range $i, $client := .Clients }} + + + + + + + + + {{ end }} + +
#NameVersionHead SlotHead RootStatus
{{ $client.Index }}{{ $client.Name }}{{ $client.Version }}{{ formatAddCommas $client.HeadSlot }}0x{{ printf "%x" $client.HeadRoot }} + {{ if eq $client.Status "ready" }} + Connected + {{ else if eq $client.Status "syncing" }} + Synchronizing + {{ else if eq $client.Status "disconnected" }} + Disconnected + {{ else }} + Unknown + {{ end }} +
+
+
+ +
+
+{{ end }} +{{ define "js" }} +{{ end }} +{{ define "css" }} +{{ end }} \ No newline at end of file diff --git a/templates/slots/slots.html b/templates/slots/slots.html index 431646a2..f31f9cc4 100644 --- a/templates/slots/slots.html +++ b/templates/slots/slots.html @@ -50,6 +50,9 @@

Slots

+ {{ if .ShowForkTree -}} + + {{- end }} @@ -71,8 +74,26 @@

Slots

{{ if gt .SlotCount 0 }} + {{ $showTree := .ShowForkTree }} + {{ $treeWidth := .ForkTreeWidth }} {{ range $i, $slot := .Slots }} + {{ if $showTree -}} + + {{- end }} {{ if eq $slot.Status 2 }} @@ -165,4 +186,5 @@

Slots

{{ define "js" }} {{ end }} {{ define "css" }} + {{ end }} \ No newline at end of file diff --git a/types/models/clients.go b/types/models/clients.go new file mode 100644 index 00000000..351ed79b --- /dev/null +++ b/types/models/clients.go @@ -0,0 +1,16 @@ +package models + +// ClientsPageData is a struct to hold info for the clients page +type ClientsPageData struct { + Clients []*ClientsPageDataClient `json:"clients"` + ClientCount uint64 `json:"client_count"` +} + +type ClientsPageDataClient struct { + Index int `json:"index"` + Name string `json:"name"` + Version string `json:"version"` + HeadSlot uint64 `json:"head_slot"` + HeadRoot []byte `json:"head_root"` + Status string `json:"status"` +} diff --git a/types/models/slots.go b/types/models/slots.go index 1f0af8f6..4416eeb0 100644 --- a/types/models/slots.go +++ b/types/models/slots.go @@ -10,6 +10,8 @@ type SlotsPageData struct { SlotCount uint64 `json:"slot_count"` FirstSlot uint64 `json:"first_slot"` LastSlot uint64 `json:"last_slot"` + ShowForkTree bool `json:"show_forktree"` + ForkTreeWidth int `json:"forktree_width"` GraffitiFilter string `json:"graffiti_filter"` IsDefaultPage bool `json:"default_page"` @@ -25,23 +27,32 @@ type SlotsPageData struct { } type SlotsPageDataSlot 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"` - Synchronized bool `json:"synchronized"` - 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"` + 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"` + Synchronized bool `json:"synchronized"` + 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"` + ParentRoot []byte `json:"parent_root"` + ForkGraph []*SlotsPageDataForkGraph `json:"fork_graph"` +} + +type SlotsPageDataForkGraph struct { + Index int `json:"index"` + Left int `json:"left"` + Tiles map[string]bool `json:"tiles"` + Block bool `json:"block"` }
GraphEpoch Slot Status
+ {{ range $j, $graph := $slot.ForkGraph }} +
+ {{- range $tile, $val := $graph.Tiles -}} +
+ {{- end -}} + {{- if $graph.Block }} +
+ +
+ {{ end -}} +
+ {{ end }} +
{{ formatAddCommas $slot.Epoch }}{{ formatAddCommas $slot.Slot }}