Skip to content

Commit eb72c7d

Browse files
committed
split builder & validator pubkey and check builder indexes for index reuse in UI
1 parent 2496721 commit eb72c7d

20 files changed

Lines changed: 261 additions & 81 deletions

handlers/builder.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,12 @@ func BuilderDetail(w http.ResponseWriter, r *http.Request) {
6969
// search by pubkey - check cache first (more accurate), then fall back to DB
7070
var pubkey phase0.BLSPubKey
7171
copy(pubkey[:], builderPubKey)
72-
if validatorIdx, found := services.GlobalBeaconService.GetValidatorIndexByPubkey(pubkey); found {
73-
idx := uint64(validatorIdx)
74-
if idx&services.BuilderIndexFlag != 0 {
75-
builderIndex = idx &^ services.BuilderIndexFlag
76-
builder = services.GlobalBeaconService.GetBuilderByIndex(gloas.BuilderIndex(builderIndex))
72+
if builderIdx, found := services.GlobalBeaconService.GetBuilderIndexByPubkey(pubkey); found {
73+
// builder indexes can be reused (EIP-8282): only accept the cache hit if this pubkey
74+
// still owns the index, otherwise fall through to the DB lookup by pubkey below.
75+
if b := services.GlobalBeaconService.GetBuilderByIndex(builderIdx); b != nil && b.PublicKey == pubkey {
76+
builderIndex = uint64(builderIdx)
77+
builder = b
7778
}
7879
}
7980

@@ -462,33 +463,32 @@ func buildBuilderRecentBids(ctx context.Context, builderIndex uint64, chainState
462463
func buildBuilderRecentDeposits(ctx context.Context, pubkey []byte, chainState *consensus.ChainState) []*models.BuilderPageDataDeposit {
463464
result := make([]*models.BuilderPageDataDeposit, 0)
464465

465-
// Query deposit requests by builder pubkey
466-
depositFilter := &services.CombinedDepositRequestFilter{
467-
Filter: &dbtypes.DepositTxFilter{
468-
PublicKey: pubkey,
469-
WithOrphaned: 1,
470-
},
466+
// Query the dedicated builder deposit requests (EIP-8282) by builder pubkey.
467+
filter := &dbtypes.BuilderDepositFilter{
468+
PublicKey: pubkey,
469+
WithOrphaned: 1,
471470
}
472-
deposits, _ := services.GlobalBeaconService.GetDepositRequestsByFilter(ctx, depositFilter, 0, 20)
471+
deposits, _, _ := services.GlobalBeaconService.GetBuilderDepositsByFilter(ctx, filter, 0, 20)
473472
for _, deposit := range deposits {
474473
entry := &models.BuilderPageDataDeposit{
475-
Type: "deposit",
476-
Amount: deposit.Amount(),
477-
DepositorAddress: deposit.SourceAddress(),
474+
Type: "deposit",
478475
}
479476
if deposit.Request != nil {
480477
entry.SlotNumber = deposit.Request.SlotNumber
481478
entry.SlotRoot = deposit.Request.SlotRoot
482479
entry.Time = chainState.SlotToTime(phase0.Slot(deposit.Request.SlotNumber))
483480
entry.Orphaned = deposit.RequestOrphaned
481+
entry.Amount = deposit.Request.Amount
484482
} else if deposit.Transaction != nil {
485-
entry.Time = chainState.SlotToTime(phase0.Slot(deposit.Transaction.BlockTime))
483+
entry.Amount = deposit.Transaction.Amount
484+
entry.Time = time.Unix(int64(deposit.Transaction.BlockTime), 0)
486485
}
487486

488487
// Add transaction details if available
489488
if deposit.Transaction != nil {
490489
entry.HasTransaction = true
491490
entry.TransactionHash = deposit.Transaction.TxHash
491+
entry.DepositorAddress = deposit.Transaction.TxSender
492492
entry.TransactionDetails = &models.BuilderPageDataDepositTxDetails{
493493
BlockNumber: deposit.Transaction.BlockNumber,
494494
BlockHash: fmt.Sprintf("%#x", deposit.Transaction.BlockRoot),

handlers/builder_deposits.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handlers
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"net/http"
@@ -12,6 +13,7 @@ import (
1213
"github.com/ethpandaops/dora/services"
1314
"github.com/ethpandaops/dora/templates"
1415
"github.com/ethpandaops/dora/types/models"
16+
"github.com/ethpandaops/go-eth2-client/spec/gloas"
1517
"github.com/ethpandaops/go-eth2-client/spec/phase0"
1618
"github.com/sirupsen/logrus"
1719
)
@@ -165,6 +167,28 @@ func buildBuilderDepositsPageData(ctx context.Context, pageIdx uint64, pageSize
165167

166168
chainState := services.GlobalBeaconService.GetChainState()
167169

170+
// builderIdxOf returns the builder index recorded for a deposit (CL request preferred, else
171+
// the pending EL tx), if any.
172+
builderIdxOf := func(deposit *services.CombinedBuilderDeposit) *uint64 {
173+
if deposit.Request != nil && deposit.Request.BuilderIndex != nil {
174+
return deposit.Request.BuilderIndex
175+
}
176+
if deposit.Transaction != nil && deposit.Transaction.BuilderIndex != nil {
177+
return deposit.Transaction.BuilderIndex
178+
}
179+
return nil
180+
}
181+
182+
// collect the builder indexes to resolve so we can batch-load the builders and tell whether
183+
// each pubkey still owns its (reusable) index or was superseded.
184+
indexes := make([]gloas.BuilderIndex, 0, len(combined))
185+
for _, deposit := range combined {
186+
if idx := builderIdxOf(deposit); idx != nil {
187+
indexes = append(indexes, gloas.BuilderIndex(*idx))
188+
}
189+
}
190+
builders := services.GlobalBeaconService.GetActiveBuildersByIndexes(ctx, indexes)
191+
168192
for _, deposit := range combined {
169193
depositData := &models.BuilderDepositsPageDataDeposit{}
170194

@@ -179,18 +203,19 @@ func buildBuilderDepositsPageData(ctx context.Context, pageIdx uint64, pageSize
179203
depositData.Amount = deposit.Request.Amount
180204
depositData.Result = deposit.Request.Result
181205
depositData.BlockNumber = deposit.Request.BlockNumber
182-
if deposit.Request.BuilderIndex != nil {
183-
depositData.HasBuilderIndex = true
184-
depositData.BuilderIndex = *deposit.Request.BuilderIndex
185-
}
186206
} else if deposit.Transaction != nil {
187207
depositData.PublicKey = deposit.Transaction.PublicKey
188208
depositData.WithdrawalCredentials = deposit.Transaction.WithdrawalCredentials
189209
depositData.Amount = deposit.Transaction.Amount
190210
depositData.BlockNumber = deposit.Transaction.BlockNumber
191-
if deposit.Transaction.BuilderIndex != nil {
211+
}
212+
213+
if idx := builderIdxOf(deposit); idx != nil {
214+
if b := builders[gloas.BuilderIndex(*idx)]; b != nil && bytes.Equal(b.PublicKey[:], depositData.PublicKey) {
192215
depositData.HasBuilderIndex = true
193-
depositData.BuilderIndex = *deposit.Transaction.BuilderIndex
216+
depositData.BuilderIndex = *idx
217+
} else {
218+
depositData.IsInactiveBuilder = true
194219
}
195220
}
196221

handlers/builder_exits.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handlers
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"net/http"
@@ -12,6 +13,7 @@ import (
1213
"github.com/ethpandaops/dora/services"
1314
"github.com/ethpandaops/dora/templates"
1415
"github.com/ethpandaops/dora/types/models"
16+
"github.com/ethpandaops/go-eth2-client/spec/gloas"
1517
"github.com/ethpandaops/go-eth2-client/spec/phase0"
1618
"github.com/sirupsen/logrus"
1719
)
@@ -153,6 +155,28 @@ func buildBuilderExitsPageData(ctx context.Context, pageIdx uint64, pageSize uin
153155

154156
chainState := services.GlobalBeaconService.GetChainState()
155157

158+
// builderIdxOf returns the builder index recorded for an exit (CL request preferred, else the
159+
// pending EL tx), if any.
160+
builderIdxOf := func(exit *services.CombinedBuilderExit) *uint64 {
161+
if exit.Request != nil && exit.Request.BuilderIndex != nil {
162+
return exit.Request.BuilderIndex
163+
}
164+
if exit.Transaction != nil && exit.Transaction.BuilderIndex != nil {
165+
return exit.Transaction.BuilderIndex
166+
}
167+
return nil
168+
}
169+
170+
// collect the builder indexes to resolve so we can batch-load the builders and tell whether
171+
// each pubkey still owns its (reusable) index or was superseded.
172+
indexes := make([]gloas.BuilderIndex, 0, len(combined))
173+
for _, exit := range combined {
174+
if idx := builderIdxOf(exit); idx != nil {
175+
indexes = append(indexes, gloas.BuilderIndex(*idx))
176+
}
177+
}
178+
builders := services.GlobalBeaconService.GetActiveBuildersByIndexes(ctx, indexes)
179+
156180
for _, exit := range combined {
157181
exitData := &models.BuilderExitsPageDataExit{}
158182

@@ -166,17 +190,18 @@ func buildBuilderExitsPageData(ctx context.Context, pageIdx uint64, pageSize uin
166190
exitData.PublicKey = exit.Request.PublicKey
167191
exitData.Result = exit.Request.Result
168192
exitData.BlockNumber = exit.Request.BlockNumber
169-
if exit.Request.BuilderIndex != nil {
170-
exitData.HasBuilderIndex = true
171-
exitData.BuilderIndex = *exit.Request.BuilderIndex
172-
}
173193
} else if exit.Transaction != nil {
174194
exitData.SourceAddress = exit.Transaction.SourceAddress
175195
exitData.PublicKey = exit.Transaction.PublicKey
176196
exitData.BlockNumber = exit.Transaction.BlockNumber
177-
if exit.Transaction.BuilderIndex != nil {
197+
}
198+
199+
if idx := builderIdxOf(exit); idx != nil {
200+
if b := builders[gloas.BuilderIndex(*idx)]; b != nil && bytes.Equal(b.PublicKey[:], exitData.PublicKey) {
178201
exitData.HasBuilderIndex = true
179-
exitData.BuilderIndex = *exit.Transaction.BuilderIndex
202+
exitData.BuilderIndex = *idx
203+
} else {
204+
exitData.IsInactiveBuilder = true
180205
}
181206
}
182207

handlers/slot.go

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handlers
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/hex"
67
"encoding/json"
@@ -981,8 +982,8 @@ func getSlotPageBlockData(ctx context.Context, blockData *services.CombinedBlock
981982
getSlotPageDepositRequests(pageData, requests.Deposits)
982983
getSlotPageWithdrawalRequests(pageData, requests.Withdrawals)
983984
getSlotPageConsolidationRequests(pageData, requests.Consolidations)
984-
getSlotPageBuilderDeposits(pageData, requests.BuilderDeposits)
985-
getSlotPageBuilderExits(pageData, requests.BuilderExits)
985+
getSlotPageBuilderDeposits(ctx, pageData, requests.BuilderDeposits)
986+
getSlotPageBuilderExits(ctx, pageData, requests.BuilderExits)
986987
}
987988
}
988989

@@ -1322,22 +1323,37 @@ func getSlotPageConsolidationRequests(pageData *models.SlotPageBlockData, consol
13221323
pageData.ConsolidationRequestsCount = uint64(len(pageData.ConsolidationRequests))
13231324
}
13241325

1325-
func getSlotPageBuilderDeposits(pageData *models.SlotPageBlockData, builderDeposits []*gloas.BuilderDepositRequest) {
1326+
func getSlotPageBuilderDeposits(ctx context.Context, pageData *models.SlotPageBlockData, builderDeposits []*gloas.BuilderDepositRequest) {
13261327
pageData.BuilderDepositRequests = make([]*models.SlotPageBuilderDepositRequest, 0, len(builderDeposits))
13271328

1328-
for _, builderDeposit := range builderDeposits {
1329+
// resolve pubkeys -> builder indexes first, then batch-load the builders for those indexes to
1330+
// tell whether each pubkey still owns its (reusable) index or was superseded.
1331+
resolvedIdx := make([]gloas.BuilderIndex, len(builderDeposits))
1332+
resolvedOk := make([]bool, len(builderDeposits))
1333+
indexes := make([]gloas.BuilderIndex, 0, len(builderDeposits))
1334+
for i, builderDeposit := range builderDeposits {
1335+
if builderIdx, found := services.GlobalBeaconService.GetBuilderIndexByPubkey(builderDeposit.Pubkey); found {
1336+
resolvedIdx[i] = builderIdx
1337+
resolvedOk[i] = true
1338+
indexes = append(indexes, builderIdx)
1339+
}
1340+
}
1341+
builders := services.GlobalBeaconService.GetActiveBuildersByIndexes(ctx, indexes)
1342+
1343+
for i, builderDeposit := range builderDeposits {
13291344
requestData := &models.SlotPageBuilderDepositRequest{
13301345
PublicKey: builderDeposit.Pubkey[:],
13311346
WithdrawalCreds: builderDeposit.WithdrawalCredentials,
13321347
Amount: uint64(builderDeposit.Amount),
13331348
Signature: builderDeposit.Signature[:],
13341349
}
13351350

1336-
if rawIdx, found := services.GlobalBeaconService.GetValidatorIndexByPubkey(builderDeposit.Pubkey); found {
1337-
fullIndex := uint64(rawIdx)
1338-
if fullIndex&services.BuilderIndexFlag != 0 {
1351+
if resolvedOk[i] {
1352+
if b := builders[resolvedIdx[i]]; b != nil && bytes.Equal(b.PublicKey[:], builderDeposit.Pubkey[:]) {
13391353
requestData.HasBuilderIndex = true
1340-
requestData.BuilderIndex = fullIndex &^ services.BuilderIndexFlag
1354+
requestData.BuilderIndex = uint64(resolvedIdx[i])
1355+
} else {
1356+
requestData.IsInactiveBuilder = true
13411357
}
13421358
}
13431359

@@ -1347,20 +1363,33 @@ func getSlotPageBuilderDeposits(pageData *models.SlotPageBlockData, builderDepos
13471363
pageData.BuilderDepositRequestsCount = uint64(len(pageData.BuilderDepositRequests))
13481364
}
13491365

1350-
func getSlotPageBuilderExits(pageData *models.SlotPageBlockData, builderExits []*gloas.BuilderExitRequest) {
1366+
func getSlotPageBuilderExits(ctx context.Context, pageData *models.SlotPageBlockData, builderExits []*gloas.BuilderExitRequest) {
13511367
pageData.BuilderExitRequests = make([]*models.SlotPageBuilderExitRequest, 0, len(builderExits))
13521368

1353-
for _, builderExit := range builderExits {
1369+
resolvedIdx := make([]gloas.BuilderIndex, len(builderExits))
1370+
resolvedOk := make([]bool, len(builderExits))
1371+
indexes := make([]gloas.BuilderIndex, 0, len(builderExits))
1372+
for i, builderExit := range builderExits {
1373+
if builderIdx, found := services.GlobalBeaconService.GetBuilderIndexByPubkey(builderExit.Pubkey); found {
1374+
resolvedIdx[i] = builderIdx
1375+
resolvedOk[i] = true
1376+
indexes = append(indexes, builderIdx)
1377+
}
1378+
}
1379+
builders := services.GlobalBeaconService.GetActiveBuildersByIndexes(ctx, indexes)
1380+
1381+
for i, builderExit := range builderExits {
13541382
requestData := &models.SlotPageBuilderExitRequest{
13551383
SourceAddress: builderExit.SourceAddress[:],
13561384
PublicKey: builderExit.Pubkey[:],
13571385
}
13581386

1359-
if rawIdx, found := services.GlobalBeaconService.GetValidatorIndexByPubkey(builderExit.Pubkey); found {
1360-
fullIndex := uint64(rawIdx)
1361-
if fullIndex&services.BuilderIndexFlag != 0 {
1387+
if resolvedOk[i] {
1388+
if b := builders[resolvedIdx[i]]; b != nil && bytes.Equal(b.PublicKey[:], builderExit.Pubkey[:]) {
13621389
requestData.HasBuilderIndex = true
1363-
requestData.BuilderIndex = fullIndex &^ services.BuilderIndexFlag
1390+
requestData.BuilderIndex = uint64(resolvedIdx[i])
1391+
} else {
1392+
requestData.IsInactiveBuilder = true
13641393
}
13651394
}
13661395

indexer/beacon/buildercache.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func (cache *builderCache) updateBuilderSet(slot phase0.Slot, dependentRoot phas
133133
cachedBuilder = &builderEntry{}
134134
cache.builderSetCache[i] = cachedBuilder
135135

136-
cache.indexer.pubkeyCache.Add(builders[i].PublicKey, phase0.ValidatorIndex(uint64(i)|BuilderIndexFlag))
136+
_ = cache.indexer.builderPubkeyCache.Add(builders[i].PublicKey, phase0.ValidatorIndex(uint64(i)))
137137
} else {
138138
parentBuilder = cachedBuilder.finalBuilder
139139
parentChecksum = cachedBuilder.finalChecksum
@@ -557,7 +557,7 @@ func (cache *builderCache) prepopulateFromDB() (uint64, error) {
557557

558558
cache.builderSetCache[dbBuilder.BuilderIndex] = builderEntry
559559

560-
cache.indexer.pubkeyCache.Add(builder.PublicKey, phase0.ValidatorIndex(dbBuilder.BuilderIndex|BuilderIndexFlag))
560+
_ = cache.indexer.builderPubkeyCache.Add(builder.PublicKey, phase0.ValidatorIndex(dbBuilder.BuilderIndex))
561561

562562
restoreCount++
563563
}

indexer/beacon/indexer.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type Indexer struct {
4747
epochCache *epochCache
4848
forkCache *forkCache
4949
pubkeyCache *pubkeyCache
50+
builderPubkeyCache *pubkeyCache
5051
validatorCache *validatorCache
5152
pendingValidators *pendingValidatorProjector
5253
validatorActivity *validatorActivityCache
@@ -123,6 +124,10 @@ func NewIndexer(ctx context.Context, logger logrus.FieldLogger, consensusPool *c
123124
indexer.epochCache = newEpochCache(indexer)
124125
indexer.forkCache = newForkCache(indexer)
125126
indexer.pubkeyCache = newPubkeyCache(indexer, utils.Config.Indexer.PubkeyCachePath)
127+
// builders (EIP-8282) live in a separate index space and may share pubkeys with validators,
128+
// so they get their own pubkey cache. It reuses the validator cache's backing store (same
129+
// file) but namespaces builder keys with a prefix to avoid collisions.
130+
indexer.builderPubkeyCache = indexer.pubkeyCache.newPrefixedCache([]byte(pubkeyCacheBuilderPrefix))
126131
indexer.validatorCache = newValidatorCache(indexer)
127132
indexer.pendingValidators = newPendingValidatorProjector(indexer)
128133
indexer.validatorActivity = newValidatorActivityCache(indexer)
@@ -486,6 +491,7 @@ func (indexer *Indexer) StopIndexer() {
486491
}
487492

488493
indexer.pubkeyCache.Close()
494+
// builderPubkeyCache shares pubkeyCache's store, so it is closed by the line above.
489495
indexer.stateCache.Close()
490496
}
491497

0 commit comments

Comments
 (0)