From be9a35d38fd1364eb87dec00c786d1220d5def99 Mon Sep 17 00:00:00 2001 From: pk910 Date: Tue, 17 Sep 2024 17:19:30 +0200 Subject: [PATCH 1/2] add withdrawal requests page --- cmd/dora-explorer/main.go | 1 + db/withdrawal_requests.go | 25 +- dbtypes/other.go | 20 +- handlers/el_withdrawals.go | 230 ++++++++++++++++++ handlers/pageData.go | 5 + indexer/beacon/block.go | 6 + services/chainservice_objects.go | 113 +++++++++ .../el_withdrawals.html} | 42 ++-- types/models/el_withdrawals.go | 50 ++++ 9 files changed, 448 insertions(+), 44 deletions(-) create mode 100644 handlers/el_withdrawals.go rename templates/{withdrawal_requests/withdrawal_requests.html => el_withdrawals/el_withdrawals.html} (86%) create mode 100644 types/models/el_withdrawals.go diff --git a/cmd/dora-explorer/main.go b/cmd/dora-explorer/main.go index b9aff762..f84febcf 100644 --- a/cmd/dora-explorer/main.go +++ b/cmd/dora-explorer/main.go @@ -162,6 +162,7 @@ func startFrontend(webserver *http.Server) { router.HandleFunc("/validators/included_deposits", handlers.IncludedDeposits).Methods("GET") router.HandleFunc("/validators/voluntary_exits", handlers.VoluntaryExits).Methods("GET") router.HandleFunc("/validators/slashings", handlers.Slashings).Methods("GET") + router.HandleFunc("/validators/el_withdrawals", handlers.ElWithdrawals).Methods("GET") router.HandleFunc("/validator/{idxOrPubKey}", handlers.Validator).Methods("GET") router.HandleFunc("/validator/{index}/slots", handlers.ValidatorSlots).Methods("GET") diff --git a/db/withdrawal_requests.go b/db/withdrawal_requests.go index 1513711f..04d6768a 100644 --- a/db/withdrawal_requests.go +++ b/db/withdrawal_requests.go @@ -70,7 +70,7 @@ func GetWithdrawalRequestsFiltered(offset uint64, limit uint32, finalizedBlock u FROM withdrawal_requests `) - if filter.SourceValidatorName != "" { + if filter.ValidatorName != "" { fmt.Fprint(&sql, ` LEFT JOIN validator_names AS source_names ON source_names."index" = withdrawal_requests.validator_index `) @@ -92,18 +92,18 @@ func GetWithdrawalRequestsFiltered(offset uint64, limit uint32, finalizedBlock u fmt.Fprintf(&sql, " %v source_address = $%v", filterOp, len(args)) filterOp = "AND" } - if filter.MinSourceIndex > 0 { - args = append(args, filter.MinSourceIndex) + if filter.MinIndex > 0 { + args = append(args, filter.MinIndex) fmt.Fprintf(&sql, " %v validator_index >= $%v", filterOp, len(args)) filterOp = "AND" } - if filter.MaxSourceIndex > 0 { - args = append(args, filter.MaxSourceIndex) + if filter.MaxIndex > 0 { + args = append(args, filter.MaxIndex) fmt.Fprintf(&sql, " %v validator_index <= $%v", filterOp, len(args)) filterOp = "AND" } - if filter.SourceValidatorName != "" { - args = append(args, "%"+filter.SourceValidatorName+"%") + if filter.ValidatorName != "" { + args = append(args, "%"+filter.ValidatorName+"%") fmt.Fprintf(&sql, " %v ", filterOp) fmt.Fprintf(&sql, EngineQuery(map[dbtypes.DBEngineType]string{ dbtypes.DBEnginePgsql: ` source_names.name ilike $%v `, @@ -111,9 +111,14 @@ func GetWithdrawalRequestsFiltered(offset uint64, limit uint32, finalizedBlock u }), len(args)) filterOp = "AND" } - if filter.Amount != nil { - args = append(args, *filter.Amount) - fmt.Fprintf(&sql, " %v amount = $%v", filterOp, len(args)) + if filter.MinAmount != nil { + args = append(args, *filter.MinAmount) + fmt.Fprintf(&sql, " %v amount >= $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.MaxAmount != nil { + args = append(args, *filter.MaxAmount) + fmt.Fprintf(&sql, " %v amount <= $%v", filterOp, len(args)) filterOp = "AND" } diff --git a/dbtypes/other.go b/dbtypes/other.go index 23ee7930..386ce1fc 100644 --- a/dbtypes/other.go +++ b/dbtypes/other.go @@ -93,15 +93,13 @@ type SlashingFilter struct { } type WithdrawalRequestFilter struct { - MinSlot uint64 - MaxSlot uint64 - SourceAddress []byte - MinSourceIndex uint64 - MaxSourceIndex uint64 - SourceValidatorName string - MinTargetIndex uint64 - MaxTargetIndex uint64 - TargetValidatorName string - Amount *uint64 - WithOrphaned uint8 + MinSlot uint64 + MaxSlot uint64 + SourceAddress []byte + MinIndex uint64 + MaxIndex uint64 + ValidatorName string + MinAmount *uint64 + MaxAmount *uint64 + WithOrphaned uint8 } diff --git a/handlers/el_withdrawals.go b/handlers/el_withdrawals.go new file mode 100644 index 00000000..5577ce88 --- /dev/null +++ b/handlers/el_withdrawals.go @@ -0,0 +1,230 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/common" + "github.com/ethpandaops/dora/dbtypes" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/templates" + "github.com/ethpandaops/dora/types/models" + "github.com/sirupsen/logrus" +) + +// ElWithdrawals will return the filtered "el_withdrawals" page using a go template +func ElWithdrawals(w http.ResponseWriter, r *http.Request) { + var templateFiles = append(layoutTemplateFiles, + "el_withdrawals/el_withdrawals.html", + "_svg/professor.html", + ) + + var pageTemplate = templates.GetTemplate(templateFiles...) + data := InitPageData(w, r, "validators", "/validators/el_withdrawals", "Withdrawal Requests", templateFiles) + + urlArgs := r.URL.Query() + var pageSize uint64 = 50 + if urlArgs.Has("c") { + pageSize, _ = strconv.ParseUint(urlArgs.Get("c"), 10, 64) + } + var pageIdx uint64 = 1 + if urlArgs.Has("p") { + pageIdx, _ = strconv.ParseUint(urlArgs.Get("p"), 10, 64) + if pageIdx < 1 { + pageIdx = 1 + } + } + + var minSlot uint64 + var maxSlot uint64 + var sourceAddr string + var minIndex uint64 + var maxIndex uint64 + var vname string + var withOrphaned uint64 + var withType uint64 + + if urlArgs.Has("f") { + if urlArgs.Has("f.mins") { + minSlot, _ = strconv.ParseUint(urlArgs.Get("f.mins"), 10, 64) + } + if urlArgs.Has("f.maxs") { + maxSlot, _ = strconv.ParseUint(urlArgs.Get("f.maxs"), 10, 64) + } + if urlArgs.Has("f.address") { + sourceAddr = urlArgs.Get("f.address") + } + if urlArgs.Has("f.mini") { + minIndex, _ = strconv.ParseUint(urlArgs.Get("f.mini"), 10, 64) + } + if urlArgs.Has("f.maxi") { + maxIndex, _ = strconv.ParseUint(urlArgs.Get("f.maxi"), 10, 64) + } + if urlArgs.Has("f.vname") { + vname = urlArgs.Get("f.vname") + } + if urlArgs.Has("f.orphaned") { + withOrphaned, _ = strconv.ParseUint(urlArgs.Get("f.orphaned"), 10, 64) + } + if urlArgs.Has("f.type") { + withType, _ = strconv.ParseUint(urlArgs.Get("f.type"), 10, 64) + } + } else { + withOrphaned = 1 + } + var pageError error + pageError = services.GlobalCallRateLimiter.CheckCallLimit(r, 2) + if pageError == nil { + data.Data, pageError = getFilteredElWithdrawalsPageData(pageIdx, pageSize, minSlot, maxSlot, sourceAddr, minIndex, maxIndex, vname, uint8(withOrphaned), uint8(withType)) + } + if pageError != nil { + handlePageError(w, r, pageError) + return + } + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "el_withdrawals.go", "ElWithdrawals", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } +} + +func getFilteredElWithdrawalsPageData(pageIdx uint64, pageSize uint64, minSlot uint64, maxSlot uint64, sourceAddr string, minIndex uint64, maxIndex uint64, vname string, withOrphaned uint8, withType uint8) (*models.ElWithdrawalsPageData, error) { + pageData := &models.ElWithdrawalsPageData{} + pageCacheKey := fmt.Sprintf("el_withdrawals:%v:%v:%v:%v:%v:%v:%v:%v:%v:%v", pageIdx, pageSize, minSlot, maxSlot, sourceAddr, minIndex, maxIndex, vname, withOrphaned, withType) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(_ *services.FrontendCacheProcessingPage) interface{} { + return buildFilteredElWithdrawalsPageData(pageIdx, pageSize, minSlot, maxSlot, sourceAddr, minIndex, maxIndex, vname, withOrphaned, withType) + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.ElWithdrawalsPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildFilteredElWithdrawalsPageData(pageIdx uint64, pageSize uint64, minSlot uint64, maxSlot uint64, sourceAddr string, minIndex uint64, maxIndex uint64, vname string, withOrphaned uint8, withType uint8) *models.ElWithdrawalsPageData { + filterArgs := url.Values{} + if minSlot != 0 { + filterArgs.Add("f.mins", fmt.Sprintf("%v", minSlot)) + } + if maxSlot != 0 { + filterArgs.Add("f.maxs", fmt.Sprintf("%v", maxSlot)) + } + if sourceAddr != "" { + filterArgs.Add("f.address", sourceAddr) + } + if minIndex != 0 { + filterArgs.Add("f.mini", fmt.Sprintf("%v", minIndex)) + } + if maxIndex != 0 { + filterArgs.Add("f.maxi", fmt.Sprintf("%v", maxIndex)) + } + if vname != "" { + filterArgs.Add("f.vname", vname) + } + if withOrphaned != 0 { + filterArgs.Add("f.orphaned", fmt.Sprintf("%v", withOrphaned)) + } + if withType != 0 { + filterArgs.Add("f.type", fmt.Sprintf("%v", withType)) + } + + pageData := &models.ElWithdrawalsPageData{ + FilterAddress: sourceAddr, + FilterMinSlot: minSlot, + FilterMaxSlot: maxSlot, + FilterMinIndex: minIndex, + FilterMaxIndex: maxIndex, + FilterValidatorName: vname, + FilterWithOrphaned: withOrphaned, + FilterWithType: withType, + } + logrus.Debugf("el_withdrawals page called: %v:%v [%v,%v,%v,%v,%v]", pageIdx, pageSize, minSlot, maxSlot, minIndex, maxIndex, vname) + if pageIdx == 1 { + pageData.IsDefaultPage = true + } + + if pageSize > 100 { + pageSize = 100 + } + pageData.PageSize = pageSize + pageData.TotalPages = pageIdx + pageData.CurrentPageIndex = pageIdx + if pageIdx > 1 { + pageData.PrevPageIndex = pageIdx - 1 + } + + // load voluntary exits + withdrawalRequestFilter := &dbtypes.WithdrawalRequestFilter{ + MinSlot: minSlot, + MaxSlot: maxSlot, + SourceAddress: common.FromHex(sourceAddr), + MinIndex: minIndex, + MaxIndex: maxIndex, + ValidatorName: vname, + WithOrphaned: withOrphaned, + } + + switch withType { + case 1: // withdrawals + minAmount := uint64(1) + withdrawalRequestFilter.MinAmount = &minAmount + case 2: // exits + maxAmount := uint64(0) + withdrawalRequestFilter.MaxAmount = &maxAmount + } + + dbElWithdrawals, totalRows := services.GlobalBeaconService.GetWithdrawalRequestsByFilter(withdrawalRequestFilter, pageIdx-1, uint32(pageSize)) + + chainState := services.GlobalBeaconService.GetChainState() + validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet() + + for _, elWithdrawal := range dbElWithdrawals { + elWithdrawalData := &models.ElWithdrawalsPageDataWithdrawal{ + SlotNumber: elWithdrawal.SlotNumber, + SlotRoot: elWithdrawal.SlotRoot, + Time: chainState.SlotToTime(phase0.Slot(elWithdrawal.SlotNumber)), + Orphaned: elWithdrawal.Orphaned, + SourceAddr: elWithdrawal.SourceAddress, + Amount: elWithdrawal.Amount, + PublicKey: elWithdrawal.ValidatorPubkey, + } + + if elWithdrawal.ValidatorIndex != nil { + elWithdrawalData.ValidatorIndex = *elWithdrawal.ValidatorIndex + elWithdrawalData.ValidatorName = services.GlobalBeaconService.GetValidatorName(*elWithdrawal.ValidatorIndex) + + if uint64(len(validatorSetRsp)) > elWithdrawalData.ValidatorIndex && validatorSetRsp[elWithdrawalData.ValidatorIndex] != nil { + elWithdrawalData.ValidatorValid = true + } + } + + pageData.ElRequests = append(pageData.ElRequests, elWithdrawalData) + } + pageData.RequestCount = uint64(len(pageData.ElRequests)) + + if pageData.RequestCount > 0 { + pageData.FirstIndex = pageData.ElRequests[0].SlotNumber + pageData.LastIndex = pageData.ElRequests[pageData.RequestCount-1].SlotNumber + } + + pageData.TotalPages = totalRows / pageSize + if totalRows%pageSize > 0 { + pageData.TotalPages++ + } + pageData.LastPageIndex = pageData.TotalPages + if pageIdx < pageData.TotalPages { + pageData.NextPageIndex = pageIdx + 1 + } + + pageData.FirstPageLink = fmt.Sprintf("/validators/el_withdrawals?f&%v&c=%v", filterArgs.Encode(), pageData.PageSize) + pageData.PrevPageLink = fmt.Sprintf("/validators/el_withdrawals?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.PrevPageIndex) + pageData.NextPageLink = fmt.Sprintf("/validators/el_withdrawals?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.NextPageIndex) + pageData.LastPageLink = fmt.Sprintf("/validators/el_withdrawals?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.LastPageIndex) + + return pageData +} diff --git a/handlers/pageData.go b/handlers/pageData.go index 0dd7c834..eab1717d 100644 --- a/handlers/pageData.go +++ b/handlers/pageData.go @@ -194,6 +194,11 @@ func createMenuItems(active string) []types.MainMenuItem { Path: "/validators/slashings", Icon: "fa-user-slash", }, + { + Label: "Withdrawal Requests", + Path: "/validators/el_withdrawals", + Icon: "fa-money-bill-transfer", + }, }, }) diff --git a/indexer/beacon/block.go b/indexer/beacon/block.go index 10bc1ca3..04488676 100644 --- a/indexer/beacon/block.go +++ b/indexer/beacon/block.go @@ -356,6 +356,12 @@ func (block *Block) GetDbSlashings(indexer *Indexer) []*dbtypes.Slashing { return indexer.dbWriter.buildDbSlashings(block, orphaned, nil) } +// GetDbWithdrawalRequests returns the database representation of the withdrawal requests in this block. +func (block *Block) GetDbWithdrawalRequests(indexer *Indexer) []*dbtypes.WithdrawalRequest { + orphaned := !indexer.IsCanonicalBlock(block, nil) + return indexer.dbWriter.buildDbWithdrawalRequests(block, orphaned, nil) +} + // GetDbConsolidationRequests returns the database representation of the consolidation requests in this block. func (block *Block) GetDbConsolidationRequests(indexer *Indexer) []*dbtypes.ConsolidationRequest { orphaned := !indexer.IsCanonicalBlock(block, nil) diff --git a/services/chainservice_objects.go b/services/chainservice_objects.go index 4791e5db..0149c394 100644 --- a/services/chainservice_objects.go +++ b/services/chainservice_objects.go @@ -370,3 +370,116 @@ func (bs *ChainService) GetSlashingsByFilter(filter *dbtypes.SlashingFilter, pag return resObjs, cachedMatchesLen + dbCount } + +func (bs *ChainService) GetWithdrawalRequestsByFilter(filter *dbtypes.WithdrawalRequestFilter, pageIdx uint64, pageSize uint32) ([]*dbtypes.WithdrawalRequest, uint64) { + chainState := bs.consensusPool.GetChainState() + finalizedBlock, prunedEpoch := bs.beaconIndexer.GetBlockCacheState() + idxMinSlot := chainState.EpochToSlot(prunedEpoch) + currentSlot := chainState.CurrentSlot() + + // load most recent objects from indexer cache + cachedMatches := make([]*dbtypes.WithdrawalRequest, 0) + for slotIdx := int64(currentSlot); slotIdx >= int64(idxMinSlot); slotIdx-- { + slot := uint64(slotIdx) + blocks := bs.beaconIndexer.GetBlocksBySlot(phase0.Slot(slot)) + if blocks != nil { + for bidx := 0; bidx < len(blocks); bidx++ { + block := blocks[bidx] + if filter.WithOrphaned != 1 { + isOrphaned := !bs.beaconIndexer.IsCanonicalBlock(block, nil) + if filter.WithOrphaned == 0 && isOrphaned { + continue + } + if filter.WithOrphaned == 2 && !isOrphaned { + continue + } + } + if filter.MinSlot > 0 && slot < filter.MinSlot { + continue + } + if filter.MaxSlot > 0 && slot > filter.MaxSlot { + continue + } + + withdrawalRequests := block.GetDbWithdrawalRequests(bs.beaconIndexer) + for idx, withdrawalRequest := range withdrawalRequests { + if filter.MinIndex > 0 && (withdrawalRequest.ValidatorIndex == nil || *withdrawalRequest.ValidatorIndex < filter.MinIndex) { + continue + } + if filter.MaxIndex > 0 && (withdrawalRequest.ValidatorIndex == nil || *withdrawalRequest.ValidatorIndex > filter.MaxIndex) { + continue + } + if filter.ValidatorName != "" { + if withdrawalRequest.ValidatorIndex == nil { + continue + } + validatorName := bs.validatorNames.GetValidatorName(*withdrawalRequest.ValidatorIndex) + if !strings.Contains(validatorName, filter.ValidatorName) { + continue + } + } + + cachedMatches = append(cachedMatches, withdrawalRequests[idx]) + } + } + } + } + + cachedMatchesLen := uint64(len(cachedMatches)) + cachedPages := cachedMatchesLen / uint64(pageSize) + resObjs := make([]*dbtypes.WithdrawalRequest, 0) + resIdx := 0 + + cachedStart := pageIdx * uint64(pageSize) + cachedEnd := cachedStart + uint64(pageSize) + + if cachedPages > 0 && pageIdx < cachedPages { + resObjs = append(resObjs, cachedMatches[cachedStart:cachedEnd]...) + resIdx += int(cachedEnd - cachedStart) + } else if pageIdx == cachedPages { + resObjs = append(resObjs, cachedMatches[cachedStart:]...) + resIdx += len(cachedMatches) - int(cachedStart) + } + + // load older objects from db + dbPage := pageIdx - cachedPages + dbCacheOffset := uint64(pageSize) - (cachedMatchesLen % uint64(pageSize)) + + var dbObjects []*dbtypes.WithdrawalRequest + var dbCount uint64 + var err error + + if resIdx > int(pageSize) { + // all results from cache, just get result count from db + _, dbCount, err = db.GetWithdrawalRequestsFiltered(0, 1, uint64(finalizedBlock), filter) + } else if dbPage == 0 { + // first page, load first `pagesize-cachedResults` items from db + dbObjects, dbCount, err = db.GetWithdrawalRequestsFiltered(0, uint32(dbCacheOffset), uint64(finalizedBlock), filter) + } else { + dbObjects, dbCount, err = db.GetWithdrawalRequestsFiltered((dbPage-1)*uint64(pageSize)+dbCacheOffset, pageSize, uint64(finalizedBlock), filter) + } + + if err != nil { + logrus.Warnf("ChainService.GetWithdrawalRequestsByFilter error: %v", err) + } else { + for idx, dbObject := range dbObjects { + if dbObject.SlotNumber > uint64(finalizedBlock) { + blockStatus := bs.CheckBlockOrphanedStatus(phase0.Root(dbObject.SlotRoot)) + dbObjects[idx].Orphaned = blockStatus == dbtypes.Orphaned + } + + if filter.WithOrphaned != 1 { + if filter.WithOrphaned == 0 && dbObjects[idx].Orphaned { + continue + } + if filter.WithOrphaned == 2 && !dbObjects[idx].Orphaned { + continue + } + } + + resObjs = append(resObjs, dbObjects[idx]) + } + } + + return resObjs, cachedMatchesLen + dbCount +} diff --git a/templates/withdrawal_requests/withdrawal_requests.html b/templates/el_withdrawals/el_withdrawals.html similarity index 86% rename from templates/withdrawal_requests/withdrawal_requests.html rename to templates/el_withdrawals/el_withdrawals.html index 44bb8d0b..026403d4 100644 --- a/templates/withdrawal_requests/withdrawal_requests.html +++ b/templates/el_withdrawals/el_withdrawals.html @@ -14,7 +14,7 @@

-
+
@@ -29,7 +29,7 @@

Address

- +
@@ -54,13 +54,13 @@

- +
-
- +
@@ -69,7 +69,7 @@

Validator Name
- +
@@ -82,9 +82,9 @@

@@ -128,7 +128,7 @@

@@ -145,9 +145,10 @@

Request Type Validator Amount + Incl. Status - Validity {{ if gt .RequestCount 0 }} @@ -162,9 +163,9 @@

{{ formatRecentTimeShort $request.Time }}
- {{ ethAddressLink $request.SourceAddress }} + {{ ethAddressLink $request.SourceAddr }}
- +
@@ -176,18 +177,19 @@

{{- end }} - {{- if $request.SourceIndexValid }} - {{ formatValidator $request.SourceIndex $request.SourceName }} + {{- if $request.ValidatorValid }} + {{ formatValidator $request.ValidatorIndex $request.ValidatorName }} {{- else }}
- 0x{{ printf "%x" $request.SourcePubkey }} + 0x{{ printf "%x" $request.PublicKey }}
- +
{{- end }} {{ formatEthFromGwei $request.Amount }} + {{- if $request.Orphaned }} Orphaned @@ -207,13 +210,6 @@

Included {{- end }} - - {{- if $request.Valid }} - ✅ - {{- else }} - ❌ - {{- end }} - {{ end }} diff --git a/types/models/el_withdrawals.go b/types/models/el_withdrawals.go new file mode 100644 index 00000000..e5a3f897 --- /dev/null +++ b/types/models/el_withdrawals.go @@ -0,0 +1,50 @@ +package models + +import ( + "time" +) + +// ElWithdrawalsPageData is a struct to hold info for the el_withdrawals page +type ElWithdrawalsPageData struct { + FilterMinSlot uint64 `json:"filter_mins"` + FilterMaxSlot uint64 `json:"filter_maxs"` + FilterAddress string `json:"filter_address"` + FilterMinIndex uint64 `json:"filter_mini"` + FilterMaxIndex uint64 `json:"filter_maxi"` + FilterValidatorName string `json:"filter_vname"` + FilterWithOrphaned uint8 `json:"filter_orphaned"` + FilterWithType uint8 `json:"filter_type"` + + ElRequests []*ElWithdrawalsPageDataWithdrawal `json:"withdrawals"` + RequestCount uint64 `json:"request_count"` + FirstIndex uint64 `json:"first_index"` + LastIndex uint64 `json:"last_index"` + + IsDefaultPage bool `json:"default_page"` + TotalPages uint64 `json:"total_pages"` + PageSize uint64 `json:"page_size"` + CurrentPageIndex uint64 `json:"page_index"` + PrevPageIndex uint64 `json:"prev_page_index"` + NextPageIndex uint64 `json:"next_page_index"` + LastPageIndex uint64 `json:"last_page_index"` + + FirstPageLink string `json:"first_page_link"` + PrevPageLink string `json:"prev_page_link"` + NextPageLink string `json:"next_page_link"` + LastPageLink string `json:"last_page_link"` +} + +type ElWithdrawalsPageDataWithdrawal struct { + SlotNumber uint64 `json:"slot"` + SlotRoot []byte `json:"slot_root"` + Time time.Time `json:"time"` + Orphaned bool `json:"orphaned"` + SourceAddr []byte `json:"source_addr"` + Amount uint64 `json:"amount"` + ValidatorValid bool `json:"vvalid"` + ValidatorIndex uint64 `json:"vindex"` + ValidatorName string `json:"vname"` + PublicKey []byte `json:"pubkey"` + LinkedTransaction bool `json:"linked_tx"` + TransactionHash []byte `json:"tx_hash"` +} From 3721783e33a8779d5a66b77d504c696166bac972 Mon Sep 17 00:00:00 2001 From: pk910 Date: Tue, 17 Sep 2024 18:50:46 +0200 Subject: [PATCH 2/2] add consolidation requests page --- cmd/dora-explorer/main.go | 1 + db/consolidation_requests.go | 123 ++++++++ dbtypes/other.go | 13 + handlers/el_consolidations.go | 249 +++++++++++++++ handlers/pageData.go | 13 +- services/chainservice_objects.go | 129 ++++++++ .../el_consolidations/el_consolidations.html | 294 ++++++++++++++++++ types/models/el_consolidations.go | 55 ++++ 8 files changed, 873 insertions(+), 4 deletions(-) create mode 100644 handlers/el_consolidations.go create mode 100644 templates/el_consolidations/el_consolidations.html create mode 100644 types/models/el_consolidations.go diff --git a/cmd/dora-explorer/main.go b/cmd/dora-explorer/main.go index f84febcf..e61232f4 100644 --- a/cmd/dora-explorer/main.go +++ b/cmd/dora-explorer/main.go @@ -163,6 +163,7 @@ func startFrontend(webserver *http.Server) { router.HandleFunc("/validators/voluntary_exits", handlers.VoluntaryExits).Methods("GET") router.HandleFunc("/validators/slashings", handlers.Slashings).Methods("GET") router.HandleFunc("/validators/el_withdrawals", handlers.ElWithdrawals).Methods("GET") + router.HandleFunc("/validators/el_consolidations", handlers.ElConsolidations).Methods("GET") router.HandleFunc("/validator/{idxOrPubKey}", handlers.Validator).Methods("GET") router.HandleFunc("/validator/{index}/slots", handlers.ValidatorSlots).Methods("GET") diff --git a/db/consolidation_requests.go b/db/consolidation_requests.go index 1c7b8e88..574c762c 100644 --- a/db/consolidation_requests.go +++ b/db/consolidation_requests.go @@ -58,3 +58,126 @@ func InsertConsolidationRequests(consolidations []*dbtypes.ConsolidationRequest, } return nil } + +func GetConsolidationRequestsFiltered(offset uint64, limit uint32, finalizedBlock uint64, filter *dbtypes.ConsolidationRequestFilter) ([]*dbtypes.ConsolidationRequest, uint64, error) { + var sql strings.Builder + args := []interface{}{} + fmt.Fprint(&sql, ` + WITH cte AS ( + SELECT + slot_number, slot_root, slot_index, orphaned, fork_id, source_address, source_index, source_pubkey, target_index, target_pubkey, tx_hash + FROM consolidation_requests + `) + + if filter.SrcValidatorName != "" { + fmt.Fprint(&sql, ` + LEFT JOIN validator_names AS source_names ON source_names."index" = consolidation_requests.source_index + `) + } + if filter.TgtValidatorName != "" { + fmt.Fprint(&sql, ` + LEFT JOIN validator_names AS target_names ON target_names."index" = consolidation_requests.target_index + `) + } + + filterOp := "WHERE" + if filter.MinSlot > 0 { + args = append(args, filter.MinSlot) + fmt.Fprintf(&sql, " %v slot_number >= $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.MaxSlot > 0 { + args = append(args, filter.MaxSlot) + fmt.Fprintf(&sql, " %v slot_number <= $%v", filterOp, len(args)) + filterOp = "AND" + } + if len(filter.SourceAddress) > 0 { + args = append(args, filter.SourceAddress) + fmt.Fprintf(&sql, " %v source_address = $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.MinSrcIndex > 0 { + args = append(args, filter.MinSrcIndex) + fmt.Fprintf(&sql, " %v source_index >= $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.MaxSrcIndex > 0 { + args = append(args, filter.MaxSrcIndex) + fmt.Fprintf(&sql, " %v source_index <= $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.MinTgtIndex > 0 { + args = append(args, filter.MinTgtIndex) + fmt.Fprintf(&sql, " %v target_index >= $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.MaxTgtIndex > 0 { + args = append(args, filter.MaxTgtIndex) + fmt.Fprintf(&sql, " %v target_index <= $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.SrcValidatorName != "" { + args = append(args, "%"+filter.SrcValidatorName+"%") + fmt.Fprintf(&sql, " %v ", filterOp) + fmt.Fprintf(&sql, EngineQuery(map[dbtypes.DBEngineType]string{ + dbtypes.DBEnginePgsql: ` source_names.name ilike $%v `, + dbtypes.DBEngineSqlite: ` source_names.name LIKE $%v `, + }), len(args)) + filterOp = "AND" + } + if filter.TgtValidatorName != "" { + args = append(args, "%"+filter.TgtValidatorName+"%") + fmt.Fprintf(&sql, " %v ", filterOp) + fmt.Fprintf(&sql, EngineQuery(map[dbtypes.DBEngineType]string{ + dbtypes.DBEnginePgsql: ` target_names.name ilike $%v `, + dbtypes.DBEngineSqlite: ` target_names.name LIKE $%v `, + }), len(args)) + filterOp = "AND" + } + + if filter.WithOrphaned == 0 { + args = append(args, finalizedBlock) + fmt.Fprintf(&sql, " %v (slot_number > $%v OR orphaned = false)", filterOp, len(args)) + filterOp = "AND" + } else if filter.WithOrphaned == 2 { + args = append(args, finalizedBlock) + fmt.Fprintf(&sql, " %v (slot_number > $%v OR orphaned = true)", filterOp, len(args)) + filterOp = "AND" + } + + args = append(args, limit) + fmt.Fprintf(&sql, `) + SELECT + count(*) AS slot_number, + null AS slot_root, + 0 AS slot_index, + false AS orphaned, + 0 AS fork_id, + null AS source_address, + 0 AS source_index, + null AS source_pubkey, + 0 AS target_index, + null AS target_pubkey, + null AS tx_hash + FROM cte + UNION ALL SELECT * FROM ( + SELECT * FROM cte + ORDER BY slot_number DESC, slot_index DESC + LIMIT $%v + `, len(args)) + + if offset > 0 { + args = append(args, offset) + fmt.Fprintf(&sql, " OFFSET $%v ", len(args)) + } + fmt.Fprintf(&sql, ") AS t1") + + consolidationRequests := []*dbtypes.ConsolidationRequest{} + err := ReaderDb.Select(&consolidationRequests, sql.String(), args...) + if err != nil { + logger.Errorf("Error while fetching filtered consolidation requests: %v", err) + return nil, 0, err + } + + return consolidationRequests[1:], consolidationRequests[0].SlotNumber, nil +} diff --git a/dbtypes/other.go b/dbtypes/other.go index 386ce1fc..1442936b 100644 --- a/dbtypes/other.go +++ b/dbtypes/other.go @@ -103,3 +103,16 @@ type WithdrawalRequestFilter struct { MaxAmount *uint64 WithOrphaned uint8 } + +type ConsolidationRequestFilter struct { + MinSlot uint64 + MaxSlot uint64 + SourceAddress []byte + MinSrcIndex uint64 + MaxSrcIndex uint64 + SrcValidatorName string + MinTgtIndex uint64 + MaxTgtIndex uint64 + TgtValidatorName string + WithOrphaned uint8 +} diff --git a/handlers/el_consolidations.go b/handlers/el_consolidations.go new file mode 100644 index 00000000..3b2e17fd --- /dev/null +++ b/handlers/el_consolidations.go @@ -0,0 +1,249 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/common" + "github.com/ethpandaops/dora/dbtypes" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/templates" + "github.com/ethpandaops/dora/types/models" + "github.com/sirupsen/logrus" +) + +// ElConsolidations will return the filtered "el_consolidations" page using a go template +func ElConsolidations(w http.ResponseWriter, r *http.Request) { + var templateFiles = append(layoutTemplateFiles, + "el_consolidations/el_consolidations.html", + "_svg/professor.html", + ) + + var pageTemplate = templates.GetTemplate(templateFiles...) + data := InitPageData(w, r, "validators", "/validators/el_consolidations", "Consolidation Requests", templateFiles) + + urlArgs := r.URL.Query() + var pageSize uint64 = 50 + if urlArgs.Has("c") { + pageSize, _ = strconv.ParseUint(urlArgs.Get("c"), 10, 64) + } + var pageIdx uint64 = 1 + if urlArgs.Has("p") { + pageIdx, _ = strconv.ParseUint(urlArgs.Get("p"), 10, 64) + if pageIdx < 1 { + pageIdx = 1 + } + } + + var minSlot uint64 + var maxSlot uint64 + var sourceAddr string + var minSrcIndex uint64 + var maxSrcIndex uint64 + var srcVName string + var minTgtIndex uint64 + var maxTgtIndex uint64 + var tgtVName string + var withOrphaned uint64 + + if urlArgs.Has("f") { + if urlArgs.Has("f.mins") { + minSlot, _ = strconv.ParseUint(urlArgs.Get("f.mins"), 10, 64) + } + if urlArgs.Has("f.maxs") { + maxSlot, _ = strconv.ParseUint(urlArgs.Get("f.maxs"), 10, 64) + } + if urlArgs.Has("f.address") { + sourceAddr = urlArgs.Get("f.address") + } + if urlArgs.Has("f.minsi") { + minSrcIndex, _ = strconv.ParseUint(urlArgs.Get("f.minsi"), 10, 64) + } + if urlArgs.Has("f.maxsi") { + maxSrcIndex, _ = strconv.ParseUint(urlArgs.Get("f.maxsi"), 10, 64) + } + if urlArgs.Has("f.svname") { + srcVName = urlArgs.Get("f.svname") + } + if urlArgs.Has("f.minti") { + minTgtIndex, _ = strconv.ParseUint(urlArgs.Get("f.minti"), 10, 64) + } + if urlArgs.Has("f.maxti") { + maxTgtIndex, _ = strconv.ParseUint(urlArgs.Get("f.maxti"), 10, 64) + } + if urlArgs.Has("f.tvname") { + tgtVName = urlArgs.Get("f.tvname") + } + if urlArgs.Has("f.orphaned") { + withOrphaned, _ = strconv.ParseUint(urlArgs.Get("f.orphaned"), 10, 64) + } + } else { + withOrphaned = 1 + } + var pageError error + pageError = services.GlobalCallRateLimiter.CheckCallLimit(r, 2) + if pageError == nil { + data.Data, pageError = getFilteredElConsolidationsPageData(pageIdx, pageSize, minSlot, maxSlot, sourceAddr, minSrcIndex, maxSrcIndex, srcVName, minTgtIndex, maxTgtIndex, tgtVName, uint8(withOrphaned)) + } + if pageError != nil { + handlePageError(w, r, pageError) + return + } + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "el_consolidations.go", "Consolidation Requests", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } +} + +func getFilteredElConsolidationsPageData(pageIdx uint64, pageSize uint64, minSlot uint64, maxSlot uint64, sourceAddr string, minSrcIndex uint64, maxSrcIndex uint64, srcVName string, minTgtIndex uint64, maxTgtIndex uint64, tgtVName string, withOrphaned uint8) (*models.ElConsolidationsPageData, error) { + pageData := &models.ElConsolidationsPageData{} + pageCacheKey := fmt.Sprintf("el_consolidations:%v:%v:%v:%v:%v:%v:%v:%v:%v:%v:%v:%v", pageIdx, pageSize, minSlot, maxSlot, sourceAddr, minSrcIndex, maxSrcIndex, srcVName, minTgtIndex, maxTgtIndex, tgtVName, withOrphaned) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(_ *services.FrontendCacheProcessingPage) interface{} { + return buildFilteredElConsolidationsPageData(pageIdx, pageSize, minSlot, maxSlot, sourceAddr, minSrcIndex, maxSrcIndex, srcVName, minTgtIndex, maxTgtIndex, tgtVName, withOrphaned) + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.ElConsolidationsPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildFilteredElConsolidationsPageData(pageIdx uint64, pageSize uint64, minSlot uint64, maxSlot uint64, sourceAddr string, minSrcIndex uint64, maxSrcIndex uint64, srcVName string, minTgtIndex uint64, maxTgtIndex uint64, tgtVName string, withOrphaned uint8) *models.ElConsolidationsPageData { + filterArgs := url.Values{} + if minSlot != 0 { + filterArgs.Add("f.mins", fmt.Sprintf("%v", minSlot)) + } + if maxSlot != 0 { + filterArgs.Add("f.maxs", fmt.Sprintf("%v", maxSlot)) + } + if sourceAddr != "" { + filterArgs.Add("f.address", sourceAddr) + } + if minSrcIndex != 0 { + filterArgs.Add("f.minsi", fmt.Sprintf("%v", minSrcIndex)) + } + if maxSrcIndex != 0 { + filterArgs.Add("f.maxsi", fmt.Sprintf("%v", maxSrcIndex)) + } + if srcVName != "" { + filterArgs.Add("f.svname", srcVName) + } + if minTgtIndex != 0 { + filterArgs.Add("f.minti", fmt.Sprintf("%v", minTgtIndex)) + } + if maxTgtIndex != 0 { + filterArgs.Add("f.maxti", fmt.Sprintf("%v", maxTgtIndex)) + } + if tgtVName != "" { + filterArgs.Add("f.tvname", tgtVName) + } + if withOrphaned != 0 { + filterArgs.Add("f.orphaned", fmt.Sprintf("%v", withOrphaned)) + } + + pageData := &models.ElConsolidationsPageData{ + FilterAddress: sourceAddr, + FilterMinSlot: minSlot, + FilterMaxSlot: maxSlot, + FilterMinSrcIndex: minSrcIndex, + FilterMaxSrcIndex: maxSrcIndex, + FilterSrcValidatorName: srcVName, + FilterMinTgtIndex: minTgtIndex, + FilterMaxTgtIndex: maxTgtIndex, + FilterTgtValidatorName: tgtVName, + FilterWithOrphaned: withOrphaned, + } + logrus.Debugf("el_consolidations page called: %v:%v [%v,%v,%v,%v,%v,%v,%v,%v]", pageIdx, pageSize, minSlot, maxSlot, minSrcIndex, maxSrcIndex, srcVName, minTgtIndex, maxTgtIndex, tgtVName) + if pageIdx == 1 { + pageData.IsDefaultPage = true + } + + if pageSize > 100 { + pageSize = 100 + } + pageData.PageSize = pageSize + pageData.TotalPages = pageIdx + pageData.CurrentPageIndex = pageIdx + if pageIdx > 1 { + pageData.PrevPageIndex = pageIdx - 1 + } + + // load voluntary exits + consolidationRequestFilter := &dbtypes.ConsolidationRequestFilter{ + MinSlot: minSlot, + MaxSlot: maxSlot, + SourceAddress: common.FromHex(sourceAddr), + MinSrcIndex: minSrcIndex, + MaxSrcIndex: maxSrcIndex, + SrcValidatorName: srcVName, + MinTgtIndex: minTgtIndex, + MaxTgtIndex: maxTgtIndex, + TgtValidatorName: tgtVName, + WithOrphaned: withOrphaned, + } + + dbElConsolidations, totalRows := services.GlobalBeaconService.GetConsolidationRequestsByFilter(consolidationRequestFilter, pageIdx-1, uint32(pageSize)) + + chainState := services.GlobalBeaconService.GetChainState() + validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet() + + for _, elConsolidation := range dbElConsolidations { + elWithdrawalData := &models.ElConsolidationsPageDataConsolidation{ + SlotNumber: elConsolidation.SlotNumber, + SlotRoot: elConsolidation.SlotRoot, + Time: chainState.SlotToTime(phase0.Slot(elConsolidation.SlotNumber)), + Orphaned: elConsolidation.Orphaned, + SourceAddr: elConsolidation.SourceAddress, + SourcePublicKey: elConsolidation.SourcePubkey, + TargetPublicKey: elConsolidation.TargetPubkey, + } + + if elConsolidation.SourceIndex != nil { + elWithdrawalData.SourceValidatorIndex = *elConsolidation.SourceIndex + elWithdrawalData.SourceValidatorName = services.GlobalBeaconService.GetValidatorName(*elConsolidation.SourceIndex) + + if uint64(len(validatorSetRsp)) > elWithdrawalData.SourceValidatorIndex && validatorSetRsp[elWithdrawalData.SourceValidatorIndex] != nil { + elWithdrawalData.SourceValidatorValid = true + } + } + + if elConsolidation.TargetIndex != nil { + elWithdrawalData.TargetValidatorIndex = *elConsolidation.TargetIndex + elWithdrawalData.TargetValidatorName = services.GlobalBeaconService.GetValidatorName(*elConsolidation.TargetIndex) + + if uint64(len(validatorSetRsp)) > elWithdrawalData.TargetValidatorIndex && validatorSetRsp[elWithdrawalData.TargetValidatorIndex] != nil { + elWithdrawalData.TargetValidatorValid = true + } + } + + pageData.ElRequests = append(pageData.ElRequests, elWithdrawalData) + } + pageData.RequestCount = uint64(len(pageData.ElRequests)) + + if pageData.RequestCount > 0 { + pageData.FirstIndex = pageData.ElRequests[0].SlotNumber + pageData.LastIndex = pageData.ElRequests[pageData.RequestCount-1].SlotNumber + } + + pageData.TotalPages = totalRows / pageSize + if totalRows%pageSize > 0 { + pageData.TotalPages++ + } + pageData.LastPageIndex = pageData.TotalPages + if pageIdx < pageData.TotalPages { + pageData.NextPageIndex = pageIdx + 1 + } + + pageData.FirstPageLink = fmt.Sprintf("/validators/el_consolidations?f&%v&c=%v", filterArgs.Encode(), pageData.PageSize) + pageData.PrevPageLink = fmt.Sprintf("/validators/el_consolidations?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.PrevPageIndex) + pageData.NextPageLink = fmt.Sprintf("/validators/el_consolidations?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.NextPageIndex) + pageData.LastPageLink = fmt.Sprintf("/validators/el_consolidations?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.LastPageIndex) + + return pageData +} diff --git a/handlers/pageData.go b/handlers/pageData.go index eab1717d..3efd36d7 100644 --- a/handlers/pageData.go +++ b/handlers/pageData.go @@ -180,10 +180,6 @@ func createMenuItems(active string) []types.MainMenuItem { Path: "/validators/deposits", Icon: "fa-file-signature", }, - }, - }) - validatorMenu = append(validatorMenu, types.NavigationGroup{ - Links: []types.NavigationLink{ { Label: "Voluntary Exits", Path: "/validators/voluntary_exits", @@ -194,11 +190,20 @@ func createMenuItems(active string) []types.MainMenuItem { Path: "/validators/slashings", Icon: "fa-user-slash", }, + }, + }) + validatorMenu = append(validatorMenu, types.NavigationGroup{ + Links: []types.NavigationLink{ { Label: "Withdrawal Requests", Path: "/validators/el_withdrawals", Icon: "fa-money-bill-transfer", }, + { + Label: "Consolidation Requests", + Path: "/validators/el_consolidations", + Icon: "fa-square-plus", + }, }, }) diff --git a/services/chainservice_objects.go b/services/chainservice_objects.go index 0149c394..bab1c214 100644 --- a/services/chainservice_objects.go +++ b/services/chainservice_objects.go @@ -483,3 +483,132 @@ func (bs *ChainService) GetWithdrawalRequestsByFilter(filter *dbtypes.Withdrawal return resObjs, cachedMatchesLen + dbCount } + +func (bs *ChainService) GetConsolidationRequestsByFilter(filter *dbtypes.ConsolidationRequestFilter, pageIdx uint64, pageSize uint32) ([]*dbtypes.ConsolidationRequest, uint64) { + chainState := bs.consensusPool.GetChainState() + finalizedBlock, prunedEpoch := bs.beaconIndexer.GetBlockCacheState() + idxMinSlot := chainState.EpochToSlot(prunedEpoch) + currentSlot := chainState.CurrentSlot() + + // load most recent objects from indexer cache + cachedMatches := make([]*dbtypes.ConsolidationRequest, 0) + for slotIdx := int64(currentSlot); slotIdx >= int64(idxMinSlot); slotIdx-- { + slot := uint64(slotIdx) + blocks := bs.beaconIndexer.GetBlocksBySlot(phase0.Slot(slot)) + if blocks != nil { + for bidx := 0; bidx < len(blocks); bidx++ { + block := blocks[bidx] + if filter.WithOrphaned != 1 { + isOrphaned := !bs.beaconIndexer.IsCanonicalBlock(block, nil) + if filter.WithOrphaned == 0 && isOrphaned { + continue + } + if filter.WithOrphaned == 2 && !isOrphaned { + continue + } + } + if filter.MinSlot > 0 && slot < filter.MinSlot { + continue + } + if filter.MaxSlot > 0 && slot > filter.MaxSlot { + continue + } + + consolidationRequests := block.GetDbConsolidationRequests(bs.beaconIndexer) + for idx, consolidationRequest := range consolidationRequests { + if filter.MinSrcIndex > 0 && (consolidationRequest.SourceIndex == nil || *consolidationRequest.SourceIndex < filter.MinSrcIndex) { + continue + } + if filter.MaxSrcIndex > 0 && (consolidationRequest.SourceIndex == nil || *consolidationRequest.SourceIndex > filter.MaxSrcIndex) { + continue + } + if filter.SrcValidatorName != "" { + if consolidationRequest.SourceIndex == nil { + continue + } + validatorName := bs.validatorNames.GetValidatorName(*consolidationRequest.SourceIndex) + if !strings.Contains(validatorName, filter.SrcValidatorName) { + continue + } + } + + if filter.MinTgtIndex > 0 && (consolidationRequest.TargetIndex == nil || *consolidationRequest.TargetIndex < filter.MinTgtIndex) { + continue + } + if filter.MaxTgtIndex > 0 && (consolidationRequest.TargetIndex == nil || *consolidationRequest.TargetIndex > filter.MaxTgtIndex) { + continue + } + if filter.TgtValidatorName != "" { + if consolidationRequest.TargetIndex == nil { + continue + } + validatorName := bs.validatorNames.GetValidatorName(*consolidationRequest.TargetIndex) + if !strings.Contains(validatorName, filter.TgtValidatorName) { + continue + } + } + + cachedMatches = append(cachedMatches, consolidationRequests[idx]) + } + } + } + } + + cachedMatchesLen := uint64(len(cachedMatches)) + cachedPages := cachedMatchesLen / uint64(pageSize) + resObjs := make([]*dbtypes.ConsolidationRequest, 0) + resIdx := 0 + + cachedStart := pageIdx * uint64(pageSize) + cachedEnd := cachedStart + uint64(pageSize) + + if cachedPages > 0 && pageIdx < cachedPages { + resObjs = append(resObjs, cachedMatches[cachedStart:cachedEnd]...) + resIdx += int(cachedEnd - cachedStart) + } else if pageIdx == cachedPages { + resObjs = append(resObjs, cachedMatches[cachedStart:]...) + resIdx += len(cachedMatches) - int(cachedStart) + } + + // load older objects from db + dbPage := pageIdx - cachedPages + dbCacheOffset := uint64(pageSize) - (cachedMatchesLen % uint64(pageSize)) + + var dbObjects []*dbtypes.ConsolidationRequest + var dbCount uint64 + var err error + + if resIdx > int(pageSize) { + // all results from cache, just get result count from db + _, dbCount, err = db.GetConsolidationRequestsFiltered(0, 1, uint64(finalizedBlock), filter) + } else if dbPage == 0 { + // first page, load first `pagesize-cachedResults` items from db + dbObjects, dbCount, err = db.GetConsolidationRequestsFiltered(0, uint32(dbCacheOffset), uint64(finalizedBlock), filter) + } else { + dbObjects, dbCount, err = db.GetConsolidationRequestsFiltered((dbPage-1)*uint64(pageSize)+dbCacheOffset, pageSize, uint64(finalizedBlock), filter) + } + + if err != nil { + logrus.Warnf("ChainService.GetConsolidationRequestsByFilter error: %v", err) + } else { + for idx, dbObject := range dbObjects { + if dbObject.SlotNumber > uint64(finalizedBlock) { + blockStatus := bs.CheckBlockOrphanedStatus(phase0.Root(dbObject.SlotRoot)) + dbObjects[idx].Orphaned = blockStatus == dbtypes.Orphaned + } + + if filter.WithOrphaned != 1 { + if filter.WithOrphaned == 0 && dbObjects[idx].Orphaned { + continue + } + if filter.WithOrphaned == 2 && !dbObjects[idx].Orphaned { + continue + } + } + + resObjs = append(resObjs, dbObjects[idx]) + } + } + + return resObjs, cachedMatchesLen + dbCount +} diff --git a/templates/el_consolidations/el_consolidations.html b/templates/el_consolidations/el_consolidations.html new file mode 100644 index 00000000..067cdbea --- /dev/null +++ b/templates/el_consolidations/el_consolidations.html @@ -0,0 +1,294 @@ +{{ define "page" }} +
+
+

+ Consolidation Requests +

+ +
+ +
+
+ +
+
+ Consolidation Request Filters +
+
+
+
+
+
+
+ Address +
+
+ +
+
+
+
+ Slot Number +
+
+
+ +
+
+ - +
+
+ +
+
+
+
+
+ Source Validator Index +
+
+
+ +
+
+ - +
+
+ +
+
+
+
+
+ Source Validator Name +
+
+ +
+
+
+
+
+
+
+
+ Orphaned +
+
+ +
+
+
+
+
+ Target Validator Index +
+
+
+ +
+
+ - +
+
+ +
+
+
+
+
+ Target Validator Name +
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + {{ if gt .RequestCount 0 }} + + {{ range $i, $request := .ElRequests }} + + {{- if $request.Orphaned }} + + {{- else }} + + {{- end }} + + + + + + + + {{ end }} + + {{ else }} + + + + + + + + {{ end }} +
SlotTimeSource AddressSource ValidatorTarget ValidatorIncl. Status
{{ formatAddCommas $request.SlotNumber }}{{ formatAddCommas $request.SlotNumber }}{{ formatRecentTimeShort $request.Time }} +
+ {{ ethAddressLink $request.SourceAddr }} +
+ +
+
+
+ {{- if $request.SourceValidatorValid }} + {{ formatValidator $request.SourceValidatorIndex $request.SourceValidatorName }} + {{- else }} +
+ 0x{{ printf "%x" $request.SourcePublicKey }} +
+ +
+
+ {{- end }} +
+ {{- if $request.TargetValidatorValid }} + {{ formatValidator $request.TargetValidatorIndex $request.TargetValidatorName }} + {{- else }} +
+ 0x{{ printf "%x" $request.TargetPublicKey }} +
+ +
+
+ {{- end }} +
+ {{- if $request.Orphaned }} + Orphaned + {{- else }} + Included + {{- end }} +
+
+ {{ template "professor_svg" }} +
+
+
+ {{ if gt .TotalPages 1 }} +
+
+
+
Showing consolidation requests from slot {{ .FirstIndex }} to {{ .LastIndex }}
+
+
+
+
+ +
+
+
+ {{ end }} +
+ +
+
+{{ end }} +{{ define "js" }} +{{ end }} +{{ define "css" }} + +{{ end }} \ No newline at end of file diff --git a/types/models/el_consolidations.go b/types/models/el_consolidations.go new file mode 100644 index 00000000..14631d1a --- /dev/null +++ b/types/models/el_consolidations.go @@ -0,0 +1,55 @@ +package models + +import ( + "time" +) + +// ElConsolidationsPageData is a struct to hold info for the el_consolidations page +type ElConsolidationsPageData struct { + FilterMinSlot uint64 `json:"filter_mins"` + FilterMaxSlot uint64 `json:"filter_maxs"` + FilterAddress string `json:"filter_address"` + FilterMinSrcIndex uint64 `json:"filter_minsi"` + FilterMaxSrcIndex uint64 `json:"filter_maxsi"` + FilterSrcValidatorName string `json:"filter_svname"` + FilterMinTgtIndex uint64 `json:"filter_minti"` + FilterMaxTgtIndex uint64 `json:"filter_maxti"` + FilterTgtValidatorName string `json:"filter_tvname"` + FilterWithOrphaned uint8 `json:"filter_orphaned"` + + ElRequests []*ElConsolidationsPageDataConsolidation `json:"consolidations"` + RequestCount uint64 `json:"request_count"` + FirstIndex uint64 `json:"first_index"` + LastIndex uint64 `json:"last_index"` + + IsDefaultPage bool `json:"default_page"` + TotalPages uint64 `json:"total_pages"` + PageSize uint64 `json:"page_size"` + CurrentPageIndex uint64 `json:"page_index"` + PrevPageIndex uint64 `json:"prev_page_index"` + NextPageIndex uint64 `json:"next_page_index"` + LastPageIndex uint64 `json:"last_page_index"` + + FirstPageLink string `json:"first_page_link"` + PrevPageLink string `json:"prev_page_link"` + NextPageLink string `json:"next_page_link"` + LastPageLink string `json:"last_page_link"` +} + +type ElConsolidationsPageDataConsolidation struct { + SlotNumber uint64 `json:"slot"` + SlotRoot []byte `json:"slot_root"` + Time time.Time `json:"time"` + Orphaned bool `json:"orphaned"` + SourceAddr []byte `json:"src_addr"` + SourceValidatorValid bool `json:"src_vvalid"` + SourceValidatorIndex uint64 `json:"src_vindex"` + SourceValidatorName string `json:"src_vname"` + SourcePublicKey []byte `json:"src_pubkey"` + TargetValidatorValid bool `json:"tgt_vvalid"` + TargetValidatorIndex uint64 `json:"tgt_vindex"` + TargetValidatorName string `json:"tgt_vname"` + TargetPublicKey []byte `json:"tgt_pubkey"` + LinkedTransaction bool `json:"linked_tx"` + TransactionHash []byte `json:"tx_hash"` +}