@@ -259,6 +259,10 @@ func buildSlotPageData(ctx context.Context, blockSlot int64, blockRoot []byte) (
259259 cacheTimeout = 10 * time .Second
260260 }
261261
262+ // Get all blocks for this slot (used for multi-block display and proposer fallback)
263+ slotBlocks , slotBlockProposers := getSlotBlocks (slot , blockRoot , blockData )
264+ pageData .SlotBlocks = slotBlocks
265+
262266 if blockData == nil {
263267 pageData .Status = uint16 (models .SlotStatusMissed )
264268 pageData .Proposer = math .MaxInt64
@@ -270,6 +274,10 @@ func buildSlotPageData(ctx context.Context, blockSlot int64, blockRoot []byte) (
270274 if pageData .Proposer == math .MaxInt64 {
271275 pageData .Proposer = db .GetSlotAssignment (uint64 (slot ))
272276 }
277+ // If proposer is still unknown, check if there's exactly one orphaned block and use its proposer
278+ if pageData .Proposer == math .MaxInt64 && len (slotBlockProposers ) == 1 {
279+ pageData .Proposer = slotBlockProposers [0 ]
280+ }
273281 pageData .ProposerName = services .GlobalBeaconService .GetValidatorName (pageData .Proposer )
274282 } else {
275283 if blockData .Orphaned {
@@ -313,6 +321,79 @@ func buildSlotPageData(ctx context.Context, blockSlot int64, blockRoot []byte) (
313321 return pageData , cacheTimeout
314322}
315323
324+ // getSlotBlocks retrieves all blocks for a given slot and builds the SlotBlocks slice
325+ // for the multi-block display. Uses GetDbBlocksByFilter which handles both cache and database.
326+ // Also returns a list of proposers from orphaned blocks (used as fallback when proposer is unknown).
327+ func getSlotBlocks (slot phase0.Slot , currentBlockRoot []byte , currentBlockData * services.CombinedBlockResponse ) ([]* models.SlotPageSlotBlock , []uint64 ) {
328+ slotBlocks := make ([]* models.SlotPageSlotBlock , 0 )
329+ orphanedProposers := make ([]uint64 , 0 )
330+ hasCanonicalOrMissed := false
331+
332+ // Get all blocks for the slot (from cache and database)
333+ slotNum := uint64 (slot )
334+ dbBlocks := services .GlobalBeaconService .GetDbBlocksByFilter (& dbtypes.BlockFilter {
335+ Slot : & slotNum ,
336+ WithOrphaned : 1 , // include both canonical and orphaned
337+ WithMissing : 1 , // include missing slots
338+ }, 0 , 100 , 0 )
339+
340+ for _ , dbBlock := range dbBlocks {
341+ if dbBlock .Block == nil {
342+ // This is a missed slot row (canonical proposer info without a block)
343+ hasCanonicalOrMissed = true
344+ slotBlocks = append (slotBlocks , & models.SlotPageSlotBlock {
345+ BlockRoot : nil , // nil indicates missed
346+ Status : uint16 (models .SlotStatusMissed ),
347+ IsCurrent : currentBlockData == nil ,
348+ })
349+ continue
350+ }
351+
352+ var blockRoot phase0.Root
353+ copy (blockRoot [:], dbBlock .Block .Root )
354+
355+ isCanonical := dbBlock .Block .Status == dbtypes .Canonical
356+ if isCanonical {
357+ hasCanonicalOrMissed = true
358+ } else {
359+ // Track orphaned block proposers for fallback
360+ orphanedProposers = append (orphanedProposers , dbBlock .Block .Proposer )
361+ }
362+
363+ isCurrent := false
364+ if currentBlockData != nil && blockRoot == currentBlockData .Root {
365+ isCurrent = true
366+ } else if len (currentBlockRoot ) == 32 && blockRoot == phase0 .Root (currentBlockRoot ) {
367+ isCurrent = true
368+ }
369+
370+ status := uint16 (models .SlotStatusOrphaned )
371+ if isCanonical {
372+ status = uint16 (models .SlotStatusFound )
373+ }
374+
375+ slotBlocks = append (slotBlocks , & models.SlotPageSlotBlock {
376+ BlockRoot : blockRoot [:],
377+ Status : status ,
378+ IsCurrent : isCurrent ,
379+ })
380+ }
381+
382+ // If no canonical or missed block was returned but there are orphaned blocks,
383+ // add a "missed (canonical)" entry (fallback for edge cases)
384+ if ! hasCanonicalOrMissed && len (slotBlocks ) > 0 {
385+ missedBlock := & models.SlotPageSlotBlock {
386+ BlockRoot : nil , // nil indicates missed
387+ Status : uint16 (models .SlotStatusMissed ),
388+ IsCurrent : currentBlockData == nil ,
389+ }
390+ // Insert missed block at the beginning
391+ slotBlocks = append ([]* models.SlotPageSlotBlock {missedBlock }, slotBlocks ... )
392+ }
393+
394+ return slotBlocks , orphanedProposers
395+ }
396+
316397func getSlotPageBlockData (blockData * services.CombinedBlockResponse , epochStatsValues * beacon.EpochStatsValues , blockUid uint64 ) * models.SlotPageBlockData {
317398 chainState := services .GlobalBeaconService .GetChainState ()
318399 specs := chainState .GetSpecs ()
0 commit comments