Skip to content

Commit

Permalink
Added chain visualization (fork graph) & clients page (#9)
Browse files Browse the repository at this point in the history
* added fork graph to slots page

* added `/clients` page

* cleanup forkgraph.css
  • Loading branch information
pk910 authored Aug 24, 2023
1 parent 75d8d19 commit 53618bf
Show file tree
Hide file tree
Showing 16 changed files with 487 additions and 24 deletions.
1 change: 1 addition & 0 deletions cmd/explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
18 changes: 18 additions & 0 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := ""
Expand Down
4 changes: 4 additions & 0 deletions db/schema/pgsql/20230720234842_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions db/schema/sqlite/20230720234842_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
68 changes: 68 additions & 0 deletions handlers/clients.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions handlers/pageData.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ func createMenuItems(active string, isMain bool) []types.MainMenuItem {
},
},
},
{
Links: []types.NavigationLink{
{
Label: "Clients",
Path: "/clients",
Icon: "fa-server",
},
},
},
},
},
}
Expand Down
131 changes: 130 additions & 1 deletion handlers/slots.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package handlers

import (
"bytes"
"fmt"
"math"
"net/http"
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
Expand Down
24 changes: 23 additions & 1 deletion indexer/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 53618bf

Please sign in to comment.