Skip to content

Commit 2d74fa0

Browse files
authored
Merge pull request #550 from ethpandaops/pk910/el-explorer-fixes-1
execution explorer fixes #1
2 parents 666ef1c + 72dd585 commit 2d74fa0

23 files changed

+694
-172
lines changed

db/el_accounts.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,43 @@ func DeleteElAccount(id uint64, dbTx *sqlx.Tx) error {
265265
return err
266266
}
267267

268+
// GetElAccountsByAddresses retrieves multiple accounts by their addresses in a single query.
269+
// Returns a map from address (as hex string) to account for efficient lookup.
270+
func GetElAccountsByAddresses(addresses [][]byte) (map[string]*dbtypes.ElAccount, error) {
271+
if len(addresses) == 0 {
272+
return make(map[string]*dbtypes.ElAccount), nil
273+
}
274+
275+
var sql strings.Builder
276+
args := make([]any, len(addresses))
277+
278+
fmt.Fprint(&sql, "SELECT id, address, funder_id, funded, is_contract, last_nonce, last_block_uid FROM el_accounts WHERE address IN (")
279+
for i, addr := range addresses {
280+
if i > 0 {
281+
fmt.Fprint(&sql, ", ")
282+
}
283+
fmt.Fprintf(&sql, "$%d", i+1)
284+
args[i] = addr
285+
}
286+
fmt.Fprint(&sql, ")")
287+
288+
accounts := []*dbtypes.ElAccount{}
289+
err := ReaderDb.Select(&accounts, sql.String(), args...)
290+
if err != nil {
291+
logger.Errorf("Error while fetching el accounts by addresses: %v", err)
292+
return nil, err
293+
}
294+
295+
// Build map from address to account
296+
result := make(map[string]*dbtypes.ElAccount, len(accounts))
297+
for _, account := range accounts {
298+
// Use hex string of address as key for efficient lookup
299+
key := fmt.Sprintf("%x", account.Address)
300+
result[key] = account
301+
}
302+
return result, nil
303+
}
304+
268305
// GetElAccountsByIDs retrieves multiple accounts by their IDs in a single query.
269306
func GetElAccountsByIDs(ids []uint64) ([]*dbtypes.ElAccount, error) {
270307
if len(ids) == 0 {

db/el_tokens.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,43 @@ func DeleteElToken(id uint64, dbTx *sqlx.Tx) error {
180180
return err
181181
}
182182

183+
// GetElTokensByContracts retrieves multiple tokens by their contract addresses in a single query.
184+
// Returns a map from contract address (as hex string) to token for efficient lookup.
185+
func GetElTokensByContracts(contracts [][]byte) (map[string]*dbtypes.ElToken, error) {
186+
if len(contracts) == 0 {
187+
return make(map[string]*dbtypes.ElToken), nil
188+
}
189+
190+
var sql strings.Builder
191+
args := make([]any, len(contracts))
192+
193+
fmt.Fprint(&sql, "SELECT id, contract, token_type, name, symbol, decimals, flags, metadata_uri, name_synced FROM el_tokens WHERE contract IN (")
194+
for i, contract := range contracts {
195+
if i > 0 {
196+
fmt.Fprint(&sql, ", ")
197+
}
198+
fmt.Fprintf(&sql, "$%d", i+1)
199+
args[i] = contract
200+
}
201+
fmt.Fprint(&sql, ")")
202+
203+
tokens := []*dbtypes.ElToken{}
204+
err := ReaderDb.Select(&tokens, sql.String(), args...)
205+
if err != nil {
206+
logger.Errorf("Error while fetching el tokens by contracts: %v", err)
207+
return nil, err
208+
}
209+
210+
// Build map from contract to token
211+
result := make(map[string]*dbtypes.ElToken, len(tokens))
212+
for _, token := range tokens {
213+
// Use hex string of contract address as key for efficient lookup
214+
key := fmt.Sprintf("%x", token.Contract)
215+
result[key] = token
216+
}
217+
return result, nil
218+
}
219+
183220
// GetElTokensByIDs retrieves multiple tokens by their IDs in a single query.
184221
func GetElTokensByIDs(ids []uint64) ([]*dbtypes.ElToken, error) {
185222
if len(ids) == 0 {

handlers/address.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func Address(w http.ResponseWriter, r *http.Request) {
6262
addressBytes, err := hex.DecodeString(addressHex)
6363
if err != nil || len(addressBytes) != 20 {
6464
data := InitPageData(w, r, "blockchain", "/address", "Address not found", notfoundTemplateFiles)
65+
data.Data = "invalid"
6566
w.Header().Set("Content-Type", "text/html")
6667
handleTemplateError(w, r, "address.go", "Address", "invalidAddress", templates.GetTemplate(notfoundTemplateFiles...).ExecuteTemplate(w, "layout", data))
6768
return
@@ -154,10 +155,9 @@ func AddressBalances(w http.ResponseWriter, r *http.Request) {
154155
}
155156

156157
// Queue balance lookups (rate limited to 10min per account)
157-
if account.ID > 0 {
158-
if txIndexer := services.GlobalBeaconService.GetTxIndexer(); txIndexer != nil {
159-
txIndexer.QueueAddressBalanceLookups(account.ID, account.Address)
160-
}
158+
// Even for unknown addresses (ID=0), we queue to check if they have balance
159+
if txIndexer := services.GlobalBeaconService.GetTxIndexer(); txIndexer != nil {
160+
txIndexer.QueueAddressBalanceLookups(account.ID, account.Address)
161161
}
162162

163163
// Build response
@@ -270,10 +270,9 @@ func buildAddressPageData(account *dbtypes.ElAccount, tabView string, pageIdx, p
270270
logrus.Debugf("address page called: %v (tab: %v)", account.ID, tabView)
271271

272272
// Queue balance lookups when page is viewed (rate limited to 10min per account)
273-
if account.ID > 0 {
274-
if txIndexer := services.GlobalBeaconService.GetTxIndexer(); txIndexer != nil {
275-
txIndexer.QueueAddressBalanceLookups(account.ID, account.Address)
276-
}
273+
// Even for unknown addresses (ID=0), we queue to check if they have balance
274+
if txIndexer := services.GlobalBeaconService.GetTxIndexer(); txIndexer != nil {
275+
txIndexer.QueueAddressBalanceLookups(account.ID, account.Address)
277276
}
278277

279278
chainState := services.GlobalBeaconService.GetChainState()

handlers/blocks.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func buildBlocksPageData(firstSlot uint64, pageSize uint64, displayColumns uint6
123123
displayMask |= 1 << (col - 1)
124124
}
125125
displayColumnsParam := ""
126-
if displayMask != 0 {
126+
if displayColumns != 0 {
127127
displayColumnsParam = fmt.Sprintf("&d=0x%x", displayMask)
128128
}
129129

@@ -186,7 +186,7 @@ func buildBlocksPageData(firstSlot uint64, pageSize uint64, displayColumns uint6
186186
// Populate UrlParams for page jump functionality
187187
pageData.UrlParams = make(map[string]string)
188188
pageData.UrlParams["c"] = fmt.Sprintf("%v", pageData.PageSize)
189-
if displayMask != 0 {
189+
if displayColumns != 0 {
190190
pageData.UrlParams["d"] = fmt.Sprintf("0x%x", displayMask)
191191
}
192192
pageData.MaxSlot = uint64(maxSlot)

handlers/slot.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
316397
func getSlotPageBlockData(blockData *services.CombinedBlockResponse, epochStatsValues *beacon.EpochStatsValues, blockUid uint64) *models.SlotPageBlockData {
317398
chainState := services.GlobalBeaconService.GetChainState()
318399
specs := chainState.GetSpecs()

handlers/slots.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func buildSlotsPageData(firstSlot uint64, pageSize uint64, displayColumns uint64
128128
displayMask |= 1 << (col - 1)
129129
}
130130
displayColumnsParam := ""
131-
if displayMask != 0 {
131+
if displayColumns != 0 {
132132
displayColumnsParam = fmt.Sprintf("&d=0x%x", displayMask)
133133
}
134134

@@ -191,7 +191,7 @@ func buildSlotsPageData(firstSlot uint64, pageSize uint64, displayColumns uint64
191191
// Populate UrlParams for page jump functionality
192192
pageData.UrlParams = make(map[string]string)
193193
pageData.UrlParams["c"] = fmt.Sprintf("%v", pageData.PageSize)
194-
if displayMask != 0 {
194+
if displayColumns != 0 {
195195
pageData.UrlParams["d"] = fmt.Sprintf("0x%x", displayMask)
196196
}
197197
pageData.MaxSlot = uint64(maxSlot)

0 commit comments

Comments
 (0)