From 2cdad85a3de46bfab3c2e834f4b154a3183796f9 Mon Sep 17 00:00:00 2001 From: Stefan Date: Wed, 22 Jul 2026 11:07:18 +0200 Subject: [PATCH 1/2] feat(index): show payload build source icon in recent slots/blocks The slots and blocks list pages already indicate the payload build source on Gloas+ blocks via the proposer icon (house = self-built, hard-hat linking to the builder = builder-built), but the landing page cards still showed the plain validator icon. Populate builder info in the index page models and use formatProposerWithBuildSource in the recent slots and recent blocks cards, both for the server-rendered rows and the knockout live-refresh path (mirrored in page-index.js). The models' Status field is narrowed to uint8 to match the formatter signature and the slots page model. --- handlers/index.go | 21 ++++++++++++++++-- static/js/page-index.js | 37 +++++++++++++++++++++++++++++++ templates/index/recentBlocks.html | 4 ++-- templates/index/recentSlots.html | 4 ++-- types/models/indexPage.go | 10 +++++++-- 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/handlers/index.go b/handlers/index.go index 2bc171c96..10e3ec6f1 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -445,6 +445,19 @@ func buildIndexPageRecentEpochsData(ctx context.Context, pageData *models.IndexP pageData.RecentEpochCount = uint64(len(pageData.RecentEpochs)) } +// resolveBuildSource maps a db builder index (-1 = self-built) to the model fields +// driving the proposer build-source icon (house / hard-hat), matching the slots page. +func resolveBuildSource(dbBuilderIndex int64) (hasBuilder bool, builderIndex uint64, builderURL string) { + if dbBuilderIndex == -1 { + return true, math.MaxUint64, "" + } + if dbBuilderIndex < 0 { + return false, 0, "" + } + builderIndex = uint64(dbBuilderIndex) + return true, builderIndex, services.GlobalBeaconService.GetBuilderURL(builderIndex) +} + func buildIndexPageRecentBlocksData(ctx context.Context, pageData *models.IndexPageData, recentBlockCount int) { pageData.RecentBlocks = make([]*models.IndexPageDataBlocks, 0) @@ -478,10 +491,11 @@ func buildIndexPageRecentBlocksData(ctx context.Context, pageData *models.IndexP Ts: chainState.SlotToTime(phase0.Slot(blockData.Slot)), Proposer: blockData.Proposer, ProposerName: services.GlobalBeaconService.GetValidatorNameAt(blockData.Proposer, phase0.Slot(blockData.Slot)), - Status: uint64(blockData.Status), + Status: uint8(blockData.Status), PayloadStatus: uint8(payloadStatus), BlockRoot: blockData.Root, } + blockModel.HasBuilder, blockModel.BuilderIndex, blockModel.BuilderURL = resolveBuildSource(blockData.BuilderIndex) if blockData.EthBlockNumber != nil { blockModel.WithEthBlock = true blockModel.EthBlock = *blockData.EthBlockNumber @@ -531,7 +545,7 @@ func buildIndexPageRecentSlotsData(ctx context.Context, pageData *models.IndexPa Slot: slot, Epoch: uint64(epoch), Ts: chainState.SlotToTime(phase0.Slot(slot)), - Status: uint64(dbSlot.Status), + Status: uint8(dbSlot.Status), PayloadStatus: uint8(payloadStatus), Safe: fcrEnabled && dbSlot.Status == dbtypes.Canonical && slot <= uint64(safeSlot), Proposer: dbSlot.Proposer, @@ -540,6 +554,9 @@ func buildIndexPageRecentSlotsData(ctx context.Context, pageData *models.IndexPa ParentRoot: dbSlot.ParentRoot, ForkGraph: make([]*models.IndexPageDataForkGraph, 0), } + if dbSlot.Status > 0 { + slotData.HasBuilder, slotData.BuilderIndex, slotData.BuilderURL = resolveBuildSource(dbSlot.BuilderIndex) + } pageData.RecentSlots = append(pageData.RecentSlots, slotData) blockCount++ buildIndexPageSlotGraph(slotData, &maxOpenFork, openForks) diff --git a/static/js/page-index.js b/static/js/page-index.js index ce67e0a51..ae7d40785 100644 --- a/static/js/page-index.js +++ b/static/js/page-index.js @@ -88,6 +88,7 @@ formatEth: function(x) { return formatFloat(x / 1000000000, 4); }, formatFloat: function(x) { return formatFloat(x, 2); }, formatValidator: function(idx, name) { return formatValidator(idx, name); }, + formatProposerWithBuildSource: function(status, idx, name, hasBuilder, builderIdx, builderUrl) { return formatProposerWithBuildSource(status, idx, name, hasBuilder, builderIdx, builderUrl); }, hexstr: function(x) { return "0x" + base64ToHex(x); }, slotStatusTooltip: function(status, payloadStatus) { var bs = ["Missed", "Canonical", "Orphaned"][status] || "Unknown"; @@ -219,6 +220,42 @@ return ` ` + idx + `` } + // mirrors utils.FormatProposerWithBuildSource: house = self-built payload, + // hard-hat (linking to the builder) = builder-built payload + function formatProposerWithBuildSource(status, idx, name, hasBuilder, builderIdx, builderUrl) { + if(status == 0 || idx >= 9223372036854775807n) { + if(idx >= 9223372036854775807n) { + return `unknown`; + } + if(name != "") { + return `` + escapeHtml(name) + ``; + } + return `` + idx + ``; + } + + if(!hasBuilder) { + return formatValidator(idx, name); + } + + var iconHtml; + if(builderIdx >= 18446744073709551615n) { + iconHtml = ``; + } else { + var builderLink = "/builder/" + builderIdx; + var external = ""; + if(builderUrl) { + builderLink = escapeHtml(builderUrl); + external = ` target="_blank" rel="noopener noreferrer"`; + } + iconHtml = ``; + } + + if(name != "") { + return `` + iconHtml + ` ` + escapeHtml(name) + ``; + } + return `` + iconHtml + ` ` + idx + ``; + } + function base64ToHex(str) { const raw = atob(str); let result = ''; diff --git a/templates/index/recentBlocks.html b/templates/index/recentBlocks.html index 64115430b..54ee688dc 100644 --- a/templates/index/recentBlocks.html +++ b/templates/index/recentBlocks.html @@ -48,7 +48,7 @@
- + {{ html "" }} {{ html "" }} @@ -82,7 +82,7 @@
{{ formatRecentTimeShort $block.Ts }} - {{ formatValidator $block.Proposer $block.ProposerName }} + {{ formatProposerWithBuildSource $block.Status $block.Proposer $block.ProposerName $block.HasBuilder $block.BuilderIndex $block.BuilderURL }} {{ end }} {{ else }} diff --git a/templates/index/recentSlots.html b/templates/index/recentSlots.html index 9fa68ca33..70b9edf8a 100644 --- a/templates/index/recentSlots.html +++ b/templates/index/recentSlots.html @@ -52,7 +52,7 @@
0"> - + @@ -109,7 +109,7 @@
{{ formatRecentTimeShort $slot.Ts }} - {{ if gt $slot.Slot 0 }}{{ formatValidator $slot.Proposer $slot.ProposerName }}{{ end }} + {{ if gt $slot.Slot 0 }}{{ formatProposerWithBuildSource $slot.Status $slot.Proposer $slot.ProposerName $slot.HasBuilder $slot.BuilderIndex $slot.BuilderURL }}{{ end }} {{ end }} {{ else }} diff --git a/types/models/indexPage.go b/types/models/indexPage.go index 9406cfa28..aa715d852 100644 --- a/types/models/indexPage.go +++ b/types/models/indexPage.go @@ -81,8 +81,11 @@ type IndexPageDataBlocks struct { Ts time.Time `json:"ts"` Proposer uint64 `json:"proposer"` ProposerName string `json:"proposer_name"` - Status uint64 `json:"status"` + Status uint8 `json:"status"` PayloadStatus uint8 `json:"payload_status"` + HasBuilder bool `json:"has_builder"` + BuilderIndex uint64 `json:"builder_index"` + BuilderURL string `json:"builder_url"` BlockRoot []byte `json:"block_root" ssz-size:"32"` } @@ -93,8 +96,11 @@ type IndexPageDataSlots struct { Ts time.Time `json:"ts"` Proposer uint64 `json:"proposer"` ProposerName string `json:"proposer_name"` - Status uint64 `json:"status"` + Status uint8 `json:"status"` PayloadStatus uint8 `json:"payload_status"` + HasBuilder bool `json:"has_builder"` + BuilderIndex uint64 `json:"builder_index"` + BuilderURL string `json:"builder_url"` Safe bool `json:"safe"` BlockRoot []byte `json:"block_root" ssz-size:"32"` ParentRoot []byte `json:"parent_root" ssz-size:"32"` From d0aab35345f8b5ad37dc07aead9dadfcfcebdc9e Mon Sep 17 00:00:00 2001 From: Stefan Date: Wed, 22 Jul 2026 12:35:01 +0200 Subject: [PATCH 2/2] docs(index): document BigInt sentinel comparisons in page-index.js MaxInt64 (unknown proposer) and MaxUint64 (self-built payload) exceed 2^53, so JSON.parse rounds them to the nearest double and equality against the exact sentinel can never match. The >= comparisons against exact BigInt literals match the rounded values instead; real indices never come near the thresholds. --- static/js/page-index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/js/page-index.js b/static/js/page-index.js index ae7d40785..6142b4cf5 100644 --- a/static/js/page-index.js +++ b/static/js/page-index.js @@ -211,6 +211,8 @@ function formatValidator(idx, name) { var icon = "fa-male mr-2"; + // MaxInt64 = "unknown proposer" sentinel. It exceeds 2^53, so JSON.parse rounds it; + // >= against the exact BigInt matches the rounded value (== would never match). if(idx >= 9223372036854775807n) { return ` unknown`; } @@ -238,6 +240,8 @@ } var iconHtml; + // MaxUint64 = "self-built payload" sentinel (db builder_index -1); rounded by JSON.parse + // above 2^53 like the proposer sentinel, hence >= against the exact BigInt. if(builderIdx >= 18446744073709551615n) { iconHtml = ``; } else {