diff --git a/cmd/dora-explorer/main.go b/cmd/dora-explorer/main.go index 24f79643..f31c8431 100644 --- a/cmd/dora-explorer/main.go +++ b/cmd/dora-explorer/main.go @@ -203,8 +203,13 @@ func startFrontend(router *mux.Router) { router.HandleFunc("/validators/included_deposits", handlers.IncludedDeposits).Methods("GET") router.HandleFunc("/validators/queued_deposits", handlers.QueuedDeposits).Methods("GET") router.HandleFunc("/validators/voluntary_exits", handlers.VoluntaryExits).Methods("GET") + router.HandleFunc("/validators/exits", handlers.Exits).Methods("GET") router.HandleFunc("/validators/slashings", handlers.Slashings).Methods("GET") router.HandleFunc("/validators/el_withdrawals", handlers.ElWithdrawals).Methods("GET") + router.HandleFunc("/validators/withdrawals", handlers.Withdrawals).Methods("GET") + router.HandleFunc("/validators/queued_withdrawals", handlers.QueuedWithdrawals).Methods("GET") + router.HandleFunc("/validators/consolidations", handlers.Consolidations).Methods("GET") + router.HandleFunc("/validators/queued_consolidations", handlers.QueuedConsolidations).Methods("GET") router.HandleFunc("/validators/el_consolidations", handlers.ElConsolidations).Methods("GET") router.HandleFunc("/validators/submit_consolidations", handlers.SubmitConsolidation).Methods("GET") router.HandleFunc("/validators/submit_withdrawals", handlers.SubmitWithdrawal).Methods("GET") diff --git a/handlers/consolidations.go b/handlers/consolidations.go new file mode 100644 index 00000000..0b5de279 --- /dev/null +++ b/handlers/consolidations.go @@ -0,0 +1,242 @@ +package handlers + +import ( + "fmt" + "math" + "net/http" + "strconv" + "strings" + "time" + + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" + "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" +) + +// Consolidations will return the main "consolidations" page using a go template +func Consolidations(w http.ResponseWriter, r *http.Request) { + var templateFiles = append(layoutTemplateFiles, + "consolidations/consolidations.html", + "_svg/professor.html", + ) + + var pageTemplate = templates.GetTemplate(templateFiles...) + data := InitPageData(w, r, "validators", "/validators/consolidations", "Consolidations", templateFiles) + + urlArgs := r.URL.Query() + var firstEpoch uint64 = math.MaxUint64 + if urlArgs.Has("epoch") { + firstEpoch, _ = strconv.ParseUint(urlArgs.Get("epoch"), 10, 64) + } + var pageSize uint64 = 50 + if urlArgs.Has("count") { + pageSize, _ = strconv.ParseUint(urlArgs.Get("count"), 10, 64) + } + + // Get tab view from URL + tabView := "recent" + if urlArgs.Has("v") { + tabView = urlArgs.Get("v") + } + + var pageError error + pageError = services.GlobalCallRateLimiter.CheckCallLimit(r, 1) + if pageError == nil { + data.Data, pageError = getConsolidationsPageData(firstEpoch, pageSize, tabView) + } + if pageError != nil { + handlePageError(w, r, pageError) + return + } + w.Header().Set("Content-Type", "text/html") + + if r.URL.Query().Has("lazy") { + // return the selected tab content only (lazy loaded) + handleTemplateError(w, r, "consolidations.go", "Consolidations", "", pageTemplate.ExecuteTemplate(w, "lazyPage", data.Data)) + } else { + handleTemplateError(w, r, "consolidations.go", "Consolidations", "", pageTemplate.ExecuteTemplate(w, "layout", data)) + } +} + +func getConsolidationsPageData(firstEpoch uint64, pageSize uint64, tabView string) (*models.ConsolidationsPageData, error) { + pageData := &models.ConsolidationsPageData{} + pageCacheKey := fmt.Sprintf("consolidations:%v:%v:%v", firstEpoch, pageSize, tabView) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(pageCall *services.FrontendCacheProcessingPage) interface{} { + pageData, cacheTimeout := buildConsolidationsPageData(firstEpoch, pageSize, tabView) + pageCall.CacheTimeout = cacheTimeout + return pageData + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.ConsolidationsPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildConsolidationsPageData(firstEpoch uint64, pageSize uint64, tabView string) (*models.ConsolidationsPageData, time.Duration) { + logrus.Debugf("consolidations page called: %v:%v:%v", firstEpoch, pageSize, tabView) + chainState := services.GlobalBeaconService.GetChainState() + epochStats, _ := services.GlobalBeaconService.GetRecentEpochStats(nil) + + pageData := &models.ConsolidationsPageData{ + TabView: tabView, + } + + // Get consolidation queue stats + consolidationRequestFilter := &services.CombinedConsolidationRequestFilter{ + Filter: &dbtypes.ConsolidationRequestFilter{ + WithOrphaned: 0, // Only canonical requests + }, + } + + // Get total consolidation count + _, _, totalConsolidations := services.GlobalBeaconService.GetConsolidationRequestsByFilter(consolidationRequestFilter, 0, 1) + pageData.TotalConsolidationCount = totalConsolidations + + // Get consolidation queue data for stats + queueFilter := &services.ConsolidationQueueFilter{ + ReverseOrder: true, + } + queuedConsolidations, queuedConsolidationCount := services.GlobalBeaconService.GetConsolidationQueueByFilter(queueFilter, 0, 1) + pageData.QueuedConsolidationCount = queuedConsolidationCount + + // Calculate consolidating validator count and amount + if epochStats != nil { + pageData.ConsolidatingValidatorCount = uint64(len(epochStats.PendingConsolidations)) + pageData.ConsolidatingAmount = uint64(epochStats.ConsolidatingBalance) + } + + // Calculate queue duration estimation based on the last queued consolidation + if len(queuedConsolidations) > 0 { + lastQueueEntry := queuedConsolidations[0] + if lastQueueEntry.SrcValidator != nil && lastQueueEntry.SrcValidator.Validator != nil { + withdrawableEpoch := lastQueueEntry.SrcValidator.Validator.WithdrawableEpoch + pageData.QueueDurationEstimate = chainState.EpochToTime(withdrawableEpoch) + pageData.HasQueueDuration = true + } + } + + // Only load data for the selected tab + switch tabView { + case "recent": + // Load recent consolidations (canonical only) + consolidationFilter := &services.CombinedConsolidationRequestFilter{ + Filter: &dbtypes.ConsolidationRequestFilter{ + WithOrphaned: 0, + }, + } + + dbConsolidations, _, _ := services.GlobalBeaconService.GetConsolidationRequestsByFilter(consolidationFilter, 0, uint32(20)) + for _, consolidation := range dbConsolidations { + consolidationData := &models.ConsolidationsPageDataRecentConsolidation{ + SourceAddr: consolidation.SourceAddress(), + SourcePublicKey: consolidation.SourcePubkey(), + TargetPublicKey: consolidation.TargetPubkey(), + } + + if sourceIndex := consolidation.SourceIndex(); sourceIndex != nil { + consolidationData.SourceValidatorIndex = *sourceIndex + consolidationData.SourceValidatorName = services.GlobalBeaconService.GetValidatorName(*sourceIndex) + consolidationData.SourceValidatorValid = true + } + + if targetIndex := consolidation.TargetIndex(); targetIndex != nil { + consolidationData.TargetValidatorIndex = *targetIndex + consolidationData.TargetValidatorName = services.GlobalBeaconService.GetValidatorName(*targetIndex) + consolidationData.TargetValidatorValid = true + } + + if request := consolidation.Request; request != nil { + consolidationData.IsIncluded = true + consolidationData.SlotNumber = request.SlotNumber + consolidationData.SlotRoot = request.SlotRoot + consolidationData.Time = chainState.SlotToTime(phase0.Slot(request.SlotNumber)) + consolidationData.Status = uint64(1) + consolidationData.Result = request.Result + consolidationData.ResultMessage = getConsolidationResultMessage(request.Result, chainState.GetSpecs()) + } + + if transaction := consolidation.Transaction; transaction != nil { + consolidationData.TransactionHash = transaction.TxHash + consolidationData.LinkedTransaction = true + consolidationData.TxStatus = uint64(1) + if consolidation.TransactionOrphaned { + consolidationData.TxStatus = uint64(2) + } + } + + pageData.RecentConsolidations = append(pageData.RecentConsolidations, consolidationData) + } + pageData.RecentConsolidationCount = uint64(len(pageData.RecentConsolidations)) + + case "queue": + // Load consolidation queue + queueConsolidations, _ := services.GlobalBeaconService.GetConsolidationQueueByFilter(&services.ConsolidationQueueFilter{}, 0, 20) + for _, queueEntry := range queueConsolidations { + queueData := &models.ConsolidationsPageDataQueuedConsolidation{} + + if queueEntry.SrcValidator != nil { + queueData.SourceValidatorExists = true + queueData.SourceValidatorIndex = uint64(queueEntry.SrcValidator.Index) + queueData.SourceValidatorName = queueEntry.SrcValidatorName + queueData.SourceEffectiveBalance = uint64(queueEntry.SrcValidator.Validator.EffectiveBalance) + + validator := services.GlobalBeaconService.GetValidatorByIndex(queueEntry.SrcValidator.Index, false) + if strings.HasPrefix(validator.Status.String(), "pending") { + queueData.SourceValidatorStatus = "Pending" + } else if validator.Status == v1.ValidatorStateActiveOngoing { + queueData.SourceValidatorStatus = "Active" + queueData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveExiting { + queueData.SourceValidatorStatus = "Exiting" + queueData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveSlashed { + queueData.SourceValidatorStatus = "Slashed" + queueData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateExitedUnslashed { + queueData.SourceValidatorStatus = "Exited" + } else if validator.Status == v1.ValidatorStateExitedSlashed { + queueData.SourceValidatorStatus = "Slashed" + } else { + queueData.SourceValidatorStatus = validator.Status.String() + } + + if queueData.ShowUpcheck { + queueData.UpcheckActivity = uint8(services.GlobalBeaconService.GetValidatorLiveness(validator.Index, 3)) + queueData.UpcheckMaximum = uint8(3) + } + + // Get public key from validator + queueData.SourcePublicKey = queueEntry.SrcValidator.Validator.PublicKey[:] + + if queueEntry.SrcValidator.Validator.WithdrawableEpoch != math.MaxUint64 { + queueData.EstimatedTime = chainState.EpochToTime(queueEntry.SrcValidator.Validator.WithdrawableEpoch) + } else { + // WithdrawableEpoch not set yet for pending consolidation + queueData.EstimatedTime = time.Time{} + } + } + + if queueEntry.TgtValidator != nil { + queueData.TargetValidatorExists = true + queueData.TargetValidatorIndex = uint64(queueEntry.TgtValidator.Index) + queueData.TargetValidatorName = queueEntry.TgtValidatorName + + // Get public key from validator + queueData.TargetPublicKey = queueEntry.TgtValidator.Validator.PublicKey[:] + } + + pageData.QueuedConsolidations = append(pageData.QueuedConsolidations, queueData) + } + pageData.QueuedTabCount = uint64(len(pageData.QueuedConsolidations)) + } + + return pageData, 1 * time.Minute +} diff --git a/handlers/exits.go b/handlers/exits.go new file mode 100644 index 00000000..fbcbef60 --- /dev/null +++ b/handlers/exits.go @@ -0,0 +1,258 @@ +package handlers + +import ( + "fmt" + "math" + "net/http" + "strconv" + "strings" + "time" + + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" + "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" +) + +// Exits will return the main "exits" page using a go template +func Exits(w http.ResponseWriter, r *http.Request) { + var templateFiles = append(layoutTemplateFiles, + "exits/exits.html", + "_svg/professor.html", + ) + + var pageTemplate = templates.GetTemplate(templateFiles...) + data := InitPageData(w, r, "validators", "/validators/exits", "Exits", templateFiles) + + urlArgs := r.URL.Query() + var firstEpoch uint64 = math.MaxUint64 + if urlArgs.Has("epoch") { + firstEpoch, _ = strconv.ParseUint(urlArgs.Get("epoch"), 10, 64) + } + var pageSize uint64 = 50 + if urlArgs.Has("count") { + pageSize, _ = strconv.ParseUint(urlArgs.Get("count"), 10, 64) + } + + // Get tab view from URL + tabView := "recent" + if urlArgs.Has("v") { + tabView = urlArgs.Get("v") + } + + var pageError error + pageError = services.GlobalCallRateLimiter.CheckCallLimit(r, 1) + if pageError == nil { + data.Data, pageError = getExitsPageData(firstEpoch, pageSize, tabView) + } + if pageError != nil { + handlePageError(w, r, pageError) + return + } + w.Header().Set("Content-Type", "text/html") + + if r.URL.Query().Has("lazy") { + // return the selected tab content only (lazy loaded) + handleTemplateError(w, r, "exits.go", "Exits", "", pageTemplate.ExecuteTemplate(w, "lazyPage", data.Data)) + } else { + handleTemplateError(w, r, "exits.go", "Exits", "", pageTemplate.ExecuteTemplate(w, "layout", data)) + } +} + +func getExitsPageData(firstEpoch uint64, pageSize uint64, tabView string) (*models.ExitsPageData, error) { + pageData := &models.ExitsPageData{} + pageCacheKey := fmt.Sprintf("exits:%v:%v:%v", firstEpoch, pageSize, tabView) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(pageCall *services.FrontendCacheProcessingPage) interface{} { + pageData, cacheTimeout := buildExitsPageData(firstEpoch, pageSize, tabView) + pageCall.CacheTimeout = cacheTimeout + return pageData + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.ExitsPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildExitsPageData(firstEpoch uint64, pageSize uint64, tabView string) (*models.ExitsPageData, time.Duration) { + logrus.Debugf("exits page called: %v:%v:%v", firstEpoch, pageSize, tabView) + chainState := services.GlobalBeaconService.GetChainState() + + pageData := &models.ExitsPageData{ + TabView: tabView, + } + + // Get total exit count from voluntary exits + voluntaryExitFilter := &dbtypes.VoluntaryExitFilter{ + WithOrphaned: 0, // Only canonical exits + } + _, totalExits := services.GlobalBeaconService.GetVoluntaryExitsByFilter(voluntaryExitFilter, 0, 1) + pageData.TotalVoluntaryExitCount = totalExits + + // Get total exit count from requested exits + zeroAmount := uint64(0) + requestedExitFilter := &services.CombinedWithdrawalRequestFilter{ + Filter: &dbtypes.WithdrawalRequestFilter{ + MaxAmount: &zeroAmount, + WithOrphaned: 0, + }, + } + _, _, totalRequestedExits := services.GlobalBeaconService.GetWithdrawalRequestsByFilter(requestedExitFilter, 0, 1) + pageData.TotalRequestedExitCount = totalRequestedExits + + // Get exiting validators (excluding consolidating validators) + validatorFilter := &dbtypes.ValidatorFilter{ + Status: []v1.ValidatorState{ + v1.ValidatorStateActiveExiting, + }, + } + + exitingValidators, _ := services.GlobalBeaconService.GetFilteredValidatorSet(validatorFilter, false) + + // Filter out consolidating validators and calculate stats + nonConsolidatingValidators := make([]*v1.Validator, 0) + var exitingAmount uint64 + var latestExitEpoch phase0.Epoch + + epochStats, _ := services.GlobalBeaconService.GetRecentEpochStats(nil) + consolidatingValidators := make(map[phase0.ValidatorIndex]bool) + if epochStats != nil { + for _, consolidation := range epochStats.PendingConsolidations { + consolidatingValidators[consolidation.SourceIndex] = true + } + } + + for _, validator := range exitingValidators { + // Skip if this validator is consolidating + if consolidatingValidators[validator.Index] { + continue + } + + nonConsolidatingValidators = append(nonConsolidatingValidators, &validator) + exitingAmount += uint64(validator.Validator.EffectiveBalance) + + if validator.Validator.ExitEpoch > latestExitEpoch { + latestExitEpoch = validator.Validator.ExitEpoch + } + } + + pageData.ExitingValidatorCount = uint64(len(nonConsolidatingValidators)) + pageData.ExitingAmount = exitingAmount + + // Calculate queue duration estimation based on the latest exit epoch + if len(nonConsolidatingValidators) > 0 && latestExitEpoch != math.MaxUint64 { + pageData.QueueDurationEstimate = chainState.EpochToTime(latestExitEpoch) + pageData.HasQueueDuration = true + } + + // Only load data for the selected tab + switch tabView { + case "recent": + // Load recent exits (canonical only) + dbVoluntaryExits, _ := services.GlobalBeaconService.GetVoluntaryExitsByFilter(voluntaryExitFilter, 0, uint32(20)) + for _, voluntaryExit := range dbVoluntaryExits { + exitData := &models.ExitsPageDataRecentExit{ + SlotNumber: voluntaryExit.SlotNumber, + SlotRoot: voluntaryExit.SlotRoot, + Time: chainState.SlotToTime(phase0.Slot(voluntaryExit.SlotNumber)), + Orphaned: voluntaryExit.Orphaned, + ValidatorIndex: voluntaryExit.ValidatorIndex, + ValidatorName: services.GlobalBeaconService.GetValidatorName(voluntaryExit.ValidatorIndex), + } + + validator := services.GlobalBeaconService.GetValidatorByIndex(phase0.ValidatorIndex(voluntaryExit.ValidatorIndex), false) + if validator == nil { + exitData.ValidatorStatus = "Unknown" + } else { + exitData.PublicKey = validator.Validator.PublicKey[:] + exitData.WithdrawalCreds = validator.Validator.WithdrawalCredentials + + if strings.HasPrefix(validator.Status.String(), "pending") { + exitData.ValidatorStatus = "Pending" + } else if validator.Status == v1.ValidatorStateActiveOngoing { + exitData.ValidatorStatus = "Active" + exitData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveExiting { + exitData.ValidatorStatus = "Exiting" + exitData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveSlashed { + exitData.ValidatorStatus = "Slashed" + exitData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateExitedUnslashed { + exitData.ValidatorStatus = "Exited" + } else if validator.Status == v1.ValidatorStateExitedSlashed { + exitData.ValidatorStatus = "Slashed" + } else { + exitData.ValidatorStatus = validator.Status.String() + } + + if exitData.ShowUpcheck { + exitData.UpcheckActivity = uint8(services.GlobalBeaconService.GetValidatorLiveness(validator.Index, 3)) + exitData.UpcheckMaximum = uint8(3) + } + } + + pageData.RecentExits = append(pageData.RecentExits, exitData) + } + pageData.RecentExitCount = uint64(len(pageData.RecentExits)) + + case "exiting": + // Load exiting validators (limit to first 20 for tab) + count := uint64(len(nonConsolidatingValidators)) + if count > 20 { + count = 20 + } + + for i := uint64(0); i < count; i++ { + validator := nonConsolidatingValidators[i] + + exitingData := &models.ExitsPageDataExitingValidator{ + ValidatorIndex: uint64(validator.Index), + ValidatorName: services.GlobalBeaconService.GetValidatorName(uint64(validator.Index)), + PublicKey: validator.Validator.PublicKey[:], + WithdrawalCreds: validator.Validator.WithdrawalCredentials, + EffectiveBalance: uint64(validator.Validator.EffectiveBalance), + ExitEpoch: uint64(validator.Validator.ExitEpoch), + } + + if strings.HasPrefix(validator.Status.String(), "pending") { + exitingData.ValidatorStatus = "Pending" + } else if validator.Status == v1.ValidatorStateActiveOngoing { + exitingData.ValidatorStatus = "Active" + exitingData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveExiting { + exitingData.ValidatorStatus = "Exiting" + exitingData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveSlashed { + exitingData.ValidatorStatus = "Slashed" + exitingData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateExitedUnslashed { + exitingData.ValidatorStatus = "Exited" + } else if validator.Status == v1.ValidatorStateExitedSlashed { + exitingData.ValidatorStatus = "Slashed" + } else { + exitingData.ValidatorStatus = validator.Status.String() + } + + if exitingData.ShowUpcheck { + exitingData.UpcheckActivity = uint8(services.GlobalBeaconService.GetValidatorLiveness(validator.Index, 3)) + exitingData.UpcheckMaximum = uint8(3) + } + + if validator.Validator.ExitEpoch != math.MaxUint64 { + exitingData.EstimatedTime = chainState.EpochToTime(validator.Validator.ExitEpoch) + } + + pageData.ExitingValidators = append(pageData.ExitingValidators, exitingData) + } + pageData.ExitingValidatorTabCount = uint64(len(pageData.ExitingValidators)) + } + + return pageData, 1 * time.Minute +} diff --git a/handlers/pageData.go b/handlers/pageData.go index 6049f1f6..6db32302 100644 --- a/handlers/pageData.go +++ b/handlers/pageData.go @@ -205,8 +205,8 @@ func createMenuItems(active string) []types.MainMenuItem { Icon: "fa-file-signature", }, { - Label: "Voluntary Exits", - Path: "/validators/voluntary_exits", + Label: "Exits", + Path: "/validators/exits", Icon: "fa-door-open", }, { @@ -224,12 +224,12 @@ func createMenuItems(active string) []types.MainMenuItem { Links: []types.NavigationLink{ { Label: "Withdrawal Requests", - Path: "/validators/el_withdrawals", + Path: "/validators/withdrawals", Icon: "fa-money-bill-transfer", }, { Label: "Consolidation Requests", - Path: "/validators/el_consolidations", + Path: "/validators/consolidations", Icon: "fa-square-plus", }, }, diff --git a/handlers/queued_consolidations.go b/handlers/queued_consolidations.go new file mode 100644 index 00000000..ef30b9a5 --- /dev/null +++ b/handlers/queued_consolidations.go @@ -0,0 +1,258 @@ +package handlers + +import ( + "fmt" + "math" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/ethereum/go-ethereum/common" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/templates" + "github.com/ethpandaops/dora/types/models" + "github.com/sirupsen/logrus" +) + +// QueuedConsolidations will return the filtered "queued_consolidations" page using a go template +func QueuedConsolidations(w http.ResponseWriter, r *http.Request) { + var templateFiles = append(layoutTemplateFiles, + "queued_consolidations/queued_consolidations.html", + "_svg/professor.html", + ) + + var pageTemplate = templates.GetTemplate(templateFiles...) + data := InitPageData(w, r, "validators", "/validators/queued_consolidations", "Queued Consolidations", 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 minSrcIndex uint64 + var maxSrcIndex uint64 + var minTgtIndex uint64 + var maxTgtIndex uint64 + var validatorName string + var pubkey string + + if urlArgs.Has("f") { + 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.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.vname") { + validatorName = urlArgs.Get("f.vname") + } + if urlArgs.Has("f.pubkey") { + pubkey = urlArgs.Get("f.pubkey") + } + } + + var pageError error + pageError = services.GlobalCallRateLimiter.CheckCallLimit(r, 2) + if pageError == nil { + data.Data, pageError = getFilteredQueuedConsolidationsPageData(pageIdx, pageSize, minSrcIndex, maxSrcIndex, minTgtIndex, maxTgtIndex, validatorName, pubkey) + } + if pageError != nil { + handlePageError(w, r, pageError) + return + } + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "queued_consolidations.go", "Queued Consolidations", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } +} + +func getFilteredQueuedConsolidationsPageData(pageIdx uint64, pageSize uint64, minSrcIndex uint64, maxSrcIndex uint64, minTgtIndex uint64, maxTgtIndex uint64, validatorName string, pubkey string) (*models.QueuedConsolidationsPageData, error) { + pageData := &models.QueuedConsolidationsPageData{} + pageCacheKey := fmt.Sprintf("queued_consolidations:%v:%v:%v:%v:%v:%v:%v:%v", pageIdx, pageSize, minSrcIndex, maxSrcIndex, minTgtIndex, maxTgtIndex, validatorName, pubkey) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(_ *services.FrontendCacheProcessingPage) interface{} { + return buildFilteredQueuedConsolidationsPageData(pageIdx, pageSize, minSrcIndex, maxSrcIndex, minTgtIndex, maxTgtIndex, validatorName, pubkey) + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.QueuedConsolidationsPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildFilteredQueuedConsolidationsPageData(pageIdx uint64, pageSize uint64, minSrcIndex uint64, maxSrcIndex uint64, minTgtIndex uint64, maxTgtIndex uint64, validatorName string, pubkey string) *models.QueuedConsolidationsPageData { + filterArgs := url.Values{} + if minSrcIndex != 0 { + filterArgs.Add("f.minsi", fmt.Sprintf("%v", minSrcIndex)) + } + if maxSrcIndex != 0 { + filterArgs.Add("f.maxsi", fmt.Sprintf("%v", maxSrcIndex)) + } + if minTgtIndex != 0 { + filterArgs.Add("f.minti", fmt.Sprintf("%v", minTgtIndex)) + } + if maxTgtIndex != 0 { + filterArgs.Add("f.maxti", fmt.Sprintf("%v", maxTgtIndex)) + } + if validatorName != "" { + filterArgs.Add("f.vname", validatorName) + } + if pubkey != "" { + filterArgs.Add("f.pubkey", pubkey) + } + + pageData := &models.QueuedConsolidationsPageData{ + FilterMinSrcIndex: minSrcIndex, + FilterMaxSrcIndex: maxSrcIndex, + FilterMinTgtIndex: minTgtIndex, + FilterMaxTgtIndex: maxTgtIndex, + FilterValidatorName: validatorName, + FilterPublicKey: pubkey, + } + + logrus.Debugf("queued_consolidations page called: %v:%v [%v,%v,%v,%v,%v,%v]", pageIdx, pageSize, minSrcIndex, maxSrcIndex, minTgtIndex, maxTgtIndex, validatorName, pubkey) + 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 + } + + // Build queue filter + queueFilter := &services.ConsolidationQueueFilter{ + ValidatorName: validatorName, + PublicKey: common.FromHex(pubkey), + } + if minSrcIndex > 0 { + queueFilter.MinSrcIndex = &minSrcIndex + } + if maxSrcIndex > 0 { + queueFilter.MaxSrcIndex = &maxSrcIndex + } + if minTgtIndex > 0 { + queueFilter.MinTgtIndex = &minTgtIndex + } + if maxTgtIndex > 0 { + queueFilter.MaxTgtIndex = &maxTgtIndex + } + + dbQueuedConsolidations, totalQueuedConsolidations := services.GlobalBeaconService.GetConsolidationQueueByFilter(queueFilter, (pageIdx-1)*pageSize, pageSize) + chainState := services.GlobalBeaconService.GetChainState() + + for _, queueEntry := range dbQueuedConsolidations { + consolidationData := &models.QueuedConsolidationsPageDataConsolidation{} + + if queueEntry.SrcValidator != nil { + consolidationData.SourceValidatorExists = true + consolidationData.SourceValidatorIndex = uint64(queueEntry.SrcValidator.Index) + consolidationData.SourceValidatorName = queueEntry.SrcValidatorName + consolidationData.SourceEffectiveBalance = uint64(queueEntry.SrcValidator.Validator.EffectiveBalance) + + validator := services.GlobalBeaconService.GetValidatorByIndex(queueEntry.SrcValidator.Index, false) + if strings.HasPrefix(validator.Status.String(), "pending") { + consolidationData.SourceValidatorStatus = "Pending" + } else if validator.Status == v1.ValidatorStateActiveOngoing { + consolidationData.SourceValidatorStatus = "Active" + consolidationData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveExiting { + consolidationData.SourceValidatorStatus = "Exiting" + consolidationData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveSlashed { + consolidationData.SourceValidatorStatus = "Slashed" + consolidationData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateExitedUnslashed { + consolidationData.SourceValidatorStatus = "Exited" + } else if validator.Status == v1.ValidatorStateExitedSlashed { + consolidationData.SourceValidatorStatus = "Slashed" + } else { + consolidationData.SourceValidatorStatus = validator.Status.String() + } + + if consolidationData.ShowUpcheck { + consolidationData.UpcheckActivity = uint8(services.GlobalBeaconService.GetValidatorLiveness(validator.Index, 3)) + consolidationData.UpcheckMaximum = uint8(3) + } + + // Get public key from validator + consolidationData.SourcePublicKey = queueEntry.SrcValidator.Validator.PublicKey[:] + + if queueEntry.SrcValidator.Validator.WithdrawableEpoch != math.MaxUint64 { + consolidationData.EstimatedTime = chainState.EpochToTime(queueEntry.SrcValidator.Validator.WithdrawableEpoch) + } else { + // WithdrawableEpoch not set yet for pending consolidation + consolidationData.EstimatedTime = time.Time{} + } + } + + if queueEntry.TgtValidator != nil { + consolidationData.TargetValidatorExists = true + consolidationData.TargetValidatorIndex = uint64(queueEntry.TgtValidator.Index) + consolidationData.TargetValidatorName = queueEntry.TgtValidatorName + + // Get public key from validator + consolidationData.TargetPublicKey = queueEntry.TgtValidator.Validator.PublicKey[:] + } + + pageData.QueuedConsolidations = append(pageData.QueuedConsolidations, consolidationData) + } + + pageData.ConsolidationCount = uint64(len(pageData.QueuedConsolidations)) + + if pageData.ConsolidationCount > 0 { + pageData.FirstIndex = pageData.QueuedConsolidations[0].SourceValidatorIndex + if len(pageData.QueuedConsolidations) > 0 { + pageData.LastIndex = pageData.QueuedConsolidations[pageData.ConsolidationCount-1].SourceValidatorIndex + } + } + + pageData.TotalPages = totalQueuedConsolidations / pageSize + if totalQueuedConsolidations%pageSize > 0 { + pageData.TotalPages++ + } + pageData.LastPageIndex = pageData.TotalPages + if pageIdx < pageData.TotalPages { + pageData.NextPageIndex = pageIdx + 1 + } + + // Populate UrlParams for page jump functionality + pageData.UrlParams = make(map[string]string) + for key, values := range filterArgs { + if len(values) > 0 { + pageData.UrlParams[key] = values[0] + } + } + pageData.UrlParams["c"] = fmt.Sprintf("%v", pageData.PageSize) + + pageData.FirstPageLink = fmt.Sprintf("/validators/queued_consolidations?f&%v&c=%v", filterArgs.Encode(), pageData.PageSize) + pageData.PrevPageLink = fmt.Sprintf("/validators/queued_consolidations?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.PrevPageIndex) + pageData.NextPageLink = fmt.Sprintf("/validators/queued_consolidations?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.NextPageIndex) + pageData.LastPageLink = fmt.Sprintf("/validators/queued_consolidations?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.LastPageIndex) + + return pageData +} diff --git a/handlers/queued_withdrawals.go b/handlers/queued_withdrawals.go new file mode 100644 index 00000000..1c84d2c1 --- /dev/null +++ b/handlers/queued_withdrawals.go @@ -0,0 +1,216 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/ethereum/go-ethereum/common" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/templates" + "github.com/ethpandaops/dora/types/models" + "github.com/sirupsen/logrus" +) + +// QueuedWithdrawals will return the filtered "queued_withdrawals" page using a go template +func QueuedWithdrawals(w http.ResponseWriter, r *http.Request) { + var templateFiles = append(layoutTemplateFiles, + "queued_withdrawals/queued_withdrawals.html", + "_svg/professor.html", + ) + + var pageTemplate = templates.GetTemplate(templateFiles...) + data := InitPageData(w, r, "validators", "/validators/queued_withdrawals", "Queued Withdrawals", 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 minIndex uint64 + var maxIndex uint64 + var validatorName string + var pubkey string + + if urlArgs.Has("f") { + 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") { + validatorName = urlArgs.Get("f.vname") + } + if urlArgs.Has("f.pubkey") { + pubkey = urlArgs.Get("f.pubkey") + } + } + + var pageError error + pageError = services.GlobalCallRateLimiter.CheckCallLimit(r, 2) + if pageError == nil { + data.Data, pageError = getFilteredQueuedWithdrawalsPageData(pageIdx, pageSize, minIndex, maxIndex, validatorName, pubkey) + } + if pageError != nil { + handlePageError(w, r, pageError) + return + } + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "queued_withdrawals.go", "Queued Withdrawals", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } +} + +func getFilteredQueuedWithdrawalsPageData(pageIdx uint64, pageSize uint64, minIndex uint64, maxIndex uint64, validatorName string, pubkey string) (*models.QueuedWithdrawalsPageData, error) { + pageData := &models.QueuedWithdrawalsPageData{} + pageCacheKey := fmt.Sprintf("queued_withdrawals:%v:%v:%v:%v:%v:%v", pageIdx, pageSize, minIndex, maxIndex, validatorName, pubkey) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(_ *services.FrontendCacheProcessingPage) interface{} { + return buildFilteredQueuedWithdrawalsPageData(pageIdx, pageSize, minIndex, maxIndex, validatorName, pubkey) + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.QueuedWithdrawalsPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildFilteredQueuedWithdrawalsPageData(pageIdx uint64, pageSize uint64, minIndex uint64, maxIndex uint64, validatorName string, pubkey string) *models.QueuedWithdrawalsPageData { + filterArgs := url.Values{} + if minIndex != 0 { + filterArgs.Add("f.mini", fmt.Sprintf("%v", minIndex)) + } + if maxIndex != 0 { + filterArgs.Add("f.maxi", fmt.Sprintf("%v", maxIndex)) + } + if validatorName != "" { + filterArgs.Add("f.vname", validatorName) + } + if pubkey != "" { + filterArgs.Add("f.pubkey", pubkey) + } + + pageData := &models.QueuedWithdrawalsPageData{ + FilterMinIndex: minIndex, + FilterMaxIndex: maxIndex, + FilterValidatorName: validatorName, + FilterPublicKey: pubkey, + } + + logrus.Debugf("queued_withdrawals page called: %v:%v [%v,%v,%v,%v]", pageIdx, pageSize, minIndex, maxIndex, validatorName, pubkey) + 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 + } + + // Build queue filter + queueFilter := &services.WithdrawalQueueFilter{ + ValidatorName: validatorName, + PublicKey: common.FromHex(pubkey), + } + if minIndex > 0 { + queueFilter.MinValidatorIndex = &minIndex + } + if maxIndex > 0 { + queueFilter.MaxValidatorIndex = &maxIndex + } + + dbQueuedWithdrawals, totalQueuedWithdrawals, _ := services.GlobalBeaconService.GetWithdrawalQueueByFilter(queueFilter, (pageIdx-1)*pageSize, uint32(pageSize)) + chainState := services.GlobalBeaconService.GetChainState() + + for _, queueEntry := range dbQueuedWithdrawals { + withdrawalData := &models.QueuedWithdrawalsPageDataWithdrawal{ + ValidatorIndex: uint64(queueEntry.ValidatorIndex), + ValidatorName: queueEntry.ValidatorName, + Amount: uint64(queueEntry.Amount), + WithdrawableEpoch: uint64(queueEntry.WithdrawableEpoch), + PublicKey: queueEntry.Validator.Validator.PublicKey[:], + } + + if strings.HasPrefix(queueEntry.Validator.Status.String(), "pending") { + withdrawalData.ValidatorStatus = "Pending" + } else if queueEntry.Validator.Status == v1.ValidatorStateActiveOngoing { + withdrawalData.ValidatorStatus = "Active" + withdrawalData.ShowUpcheck = true + } else if queueEntry.Validator.Status == v1.ValidatorStateActiveExiting { + withdrawalData.ValidatorStatus = "Exiting" + withdrawalData.ShowUpcheck = true + } else if queueEntry.Validator.Status == v1.ValidatorStateActiveSlashed { + withdrawalData.ValidatorStatus = "Slashed" + withdrawalData.ShowUpcheck = true + } else if queueEntry.Validator.Status == v1.ValidatorStateExitedUnslashed { + withdrawalData.ValidatorStatus = "Exited" + } else if queueEntry.Validator.Status == v1.ValidatorStateExitedSlashed { + withdrawalData.ValidatorStatus = "Slashed" + } else { + withdrawalData.ValidatorStatus = queueEntry.Validator.Status.String() + } + + if withdrawalData.ShowUpcheck { + withdrawalData.UpcheckActivity = uint8(services.GlobalBeaconService.GetValidatorLiveness(queueEntry.Validator.Index, 3)) + withdrawalData.UpcheckMaximum = uint8(3) + } + + // Use the calculated EstimatedWithdrawalTime from the queue entry + withdrawalData.EstimatedTime = chainState.SlotToTime(queueEntry.EstimatedWithdrawalTime) + + pageData.QueuedWithdrawals = append(pageData.QueuedWithdrawals, withdrawalData) + } + + pageData.WithdrawalCount = uint64(len(pageData.QueuedWithdrawals)) + + if pageData.WithdrawalCount > 0 { + pageData.FirstIndex = pageData.QueuedWithdrawals[0].ValidatorIndex + if len(pageData.QueuedWithdrawals) > 0 { + pageData.LastIndex = pageData.QueuedWithdrawals[pageData.WithdrawalCount-1].ValidatorIndex + } + } + + pageData.TotalPages = totalQueuedWithdrawals / pageSize + if totalQueuedWithdrawals%pageSize > 0 { + pageData.TotalPages++ + } + pageData.LastPageIndex = pageData.TotalPages + if pageIdx < pageData.TotalPages { + pageData.NextPageIndex = pageIdx + 1 + } + + // Populate UrlParams for page jump functionality + pageData.UrlParams = make(map[string]string) + for key, values := range filterArgs { + if len(values) > 0 { + pageData.UrlParams[key] = values[0] + } + } + pageData.UrlParams["c"] = fmt.Sprintf("%v", pageData.PageSize) + + pageData.FirstPageLink = fmt.Sprintf("/validators/queued_withdrawals?f&%v&c=%v", filterArgs.Encode(), pageData.PageSize) + pageData.PrevPageLink = fmt.Sprintf("/validators/queued_withdrawals?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.PrevPageIndex) + pageData.NextPageLink = fmt.Sprintf("/validators/queued_withdrawals?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.NextPageIndex) + pageData.LastPageLink = fmt.Sprintf("/validators/queued_withdrawals?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.LastPageIndex) + + return pageData +} diff --git a/handlers/withdrawals.go b/handlers/withdrawals.go new file mode 100644 index 00000000..83f0ffb0 --- /dev/null +++ b/handlers/withdrawals.go @@ -0,0 +1,216 @@ +package handlers + +import ( + "fmt" + "math" + "net/http" + "strconv" + "strings" + "time" + + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" + "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" +) + +// Withdrawals will return the main "withdrawals" page using a go template +func Withdrawals(w http.ResponseWriter, r *http.Request) { + var templateFiles = append(layoutTemplateFiles, + "withdrawals/withdrawals.html", + "_svg/professor.html", + ) + + var pageTemplate = templates.GetTemplate(templateFiles...) + data := InitPageData(w, r, "validators", "/validators/withdrawals", "Withdrawals", templateFiles) + + urlArgs := r.URL.Query() + var firstEpoch uint64 = math.MaxUint64 + if urlArgs.Has("epoch") { + firstEpoch, _ = strconv.ParseUint(urlArgs.Get("epoch"), 10, 64) + } + var pageSize uint64 = 50 + if urlArgs.Has("count") { + pageSize, _ = strconv.ParseUint(urlArgs.Get("count"), 10, 64) + } + + // Get tab view from URL + tabView := "recent" + if urlArgs.Has("v") { + tabView = urlArgs.Get("v") + } + + var pageError error + pageError = services.GlobalCallRateLimiter.CheckCallLimit(r, 1) + if pageError == nil { + data.Data, pageError = getWithdrawalsPageData(firstEpoch, pageSize, tabView) + } + if pageError != nil { + handlePageError(w, r, pageError) + return + } + w.Header().Set("Content-Type", "text/html") + + if r.URL.Query().Has("lazy") { + // return the selected tab content only (lazy loaded) + handleTemplateError(w, r, "withdrawals.go", "Withdrawals", "", pageTemplate.ExecuteTemplate(w, "lazyPage", data.Data)) + } else { + handleTemplateError(w, r, "withdrawals.go", "Withdrawals", "", pageTemplate.ExecuteTemplate(w, "layout", data)) + } +} + +func getWithdrawalsPageData(firstEpoch uint64, pageSize uint64, tabView string) (*models.WithdrawalsPageData, error) { + pageData := &models.WithdrawalsPageData{} + pageCacheKey := fmt.Sprintf("withdrawals:%v:%v:%v", firstEpoch, pageSize, tabView) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(pageCall *services.FrontendCacheProcessingPage) interface{} { + pageData, cacheTimeout := buildWithdrawalsPageData(firstEpoch, pageSize, tabView) + pageCall.CacheTimeout = cacheTimeout + return pageData + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.WithdrawalsPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildWithdrawalsPageData(firstEpoch uint64, pageSize uint64, tabView string) (*models.WithdrawalsPageData, time.Duration) { + logrus.Debugf("withdrawals page called: %v:%v:%v", firstEpoch, pageSize, tabView) + chainState := services.GlobalBeaconService.GetChainState() + + pageData := &models.WithdrawalsPageData{ + TabView: tabView, + } + + // Get withdrawal queue data for stats + queueFilter := &services.WithdrawalQueueFilter{} + queuedWithdrawals, queuedWithdrawalCount, queuedAmount := services.GlobalBeaconService.GetWithdrawalQueueByFilter(queueFilter, 0, 1) + pageData.QueuedWithdrawalCount = queuedWithdrawalCount + pageData.WithdrawingValidatorCount = queuedWithdrawalCount + pageData.WithdrawingAmount = uint64(queuedAmount) + + // Calculate queue duration estimation based on the last queued withdrawal + if len(queuedWithdrawals) > 0 { + lastQueueEntry := queuedWithdrawals[len(queuedWithdrawals)-1] + pageData.QueueDurationEstimate = chainState.SlotToTime(lastQueueEntry.EstimatedWithdrawalTime) + pageData.HasQueueDuration = true + } + + zeroAmount := uint64(0) + oneAmount := uint64(1) + + _, _, totalWithdrawals := services.GlobalBeaconService.GetWithdrawalRequestsByFilter(&services.CombinedWithdrawalRequestFilter{ + Filter: &dbtypes.WithdrawalRequestFilter{ + WithOrphaned: 1, + MinAmount: &oneAmount, + }, + }, 0, 1) + pageData.TotalWithdrawalCount = totalWithdrawals + + _, _, totalExits := services.GlobalBeaconService.GetWithdrawalRequestsByFilter(&services.CombinedWithdrawalRequestFilter{ + Filter: &dbtypes.WithdrawalRequestFilter{ + WithOrphaned: 1, + + MaxAmount: &zeroAmount, + }, + }, 0, 1) + pageData.TotalExitCount = totalExits + + // Only load data for the selected tab + switch tabView { + case "recent": + withdrawalFilter := &services.CombinedWithdrawalRequestFilter{ + Filter: &dbtypes.WithdrawalRequestFilter{ + WithOrphaned: 1, + }, + } + + dbWithdrawals, _, _ := services.GlobalBeaconService.GetWithdrawalRequestsByFilter(withdrawalFilter, 0, uint32(20)) + for _, withdrawal := range dbWithdrawals { + withdrawalData := &models.WithdrawalsPageDataRecentWithdrawal{ + SourceAddr: withdrawal.SourceAddress(), + Amount: withdrawal.Amount(), + PublicKey: withdrawal.ValidatorPubkey(), + } + + if validatorIndex := withdrawal.ValidatorIndex(); validatorIndex != nil { + withdrawalData.ValidatorIndex = *validatorIndex + withdrawalData.ValidatorName = services.GlobalBeaconService.GetValidatorName(*validatorIndex) + withdrawalData.ValidatorValid = true + } + + if request := withdrawal.Request; request != nil { + withdrawalData.IsIncluded = true + withdrawalData.SlotNumber = request.SlotNumber + withdrawalData.SlotRoot = request.SlotRoot + withdrawalData.Time = chainState.SlotToTime(phase0.Slot(request.SlotNumber)) + withdrawalData.Status = uint64(1) + withdrawalData.Result = request.Result + withdrawalData.ResultMessage = getWithdrawalResultMessage(request.Result, chainState.GetSpecs()) + } + + if transaction := withdrawal.Transaction; transaction != nil { + withdrawalData.TransactionHash = transaction.TxHash + withdrawalData.LinkedTransaction = true + withdrawalData.TxStatus = uint64(1) + if withdrawal.TransactionOrphaned { + withdrawalData.TxStatus = uint64(2) + } + } + + pageData.RecentWithdrawals = append(pageData.RecentWithdrawals, withdrawalData) + } + pageData.RecentWithdrawalCount = uint64(len(pageData.RecentWithdrawals)) + + case "queue": + // Load withdrawal queue + queueWithdrawals, _, _ := services.GlobalBeaconService.GetWithdrawalQueueByFilter(&services.WithdrawalQueueFilter{}, 0, 20) + for _, queueEntry := range queueWithdrawals { + queueData := &models.WithdrawalsPageDataQueuedWithdrawal{ + ValidatorIndex: uint64(queueEntry.ValidatorIndex), + ValidatorName: queueEntry.ValidatorName, + Amount: uint64(queueEntry.Amount), + WithdrawableEpoch: uint64(queueEntry.WithdrawableEpoch), + PublicKey: queueEntry.Validator.Validator.PublicKey[:], + } + + if strings.HasPrefix(queueEntry.Validator.Status.String(), "pending") { + queueData.ValidatorStatus = "Pending" + } else if queueEntry.Validator.Status == v1.ValidatorStateActiveOngoing { + queueData.ValidatorStatus = "Active" + queueData.ShowUpcheck = true + } else if queueEntry.Validator.Status == v1.ValidatorStateActiveExiting { + queueData.ValidatorStatus = "Exiting" + queueData.ShowUpcheck = true + } else if queueEntry.Validator.Status == v1.ValidatorStateActiveSlashed { + queueData.ValidatorStatus = "Slashed" + queueData.ShowUpcheck = true + } else if queueEntry.Validator.Status == v1.ValidatorStateExitedUnslashed { + queueData.ValidatorStatus = "Exited" + } else if queueEntry.Validator.Status == v1.ValidatorStateExitedSlashed { + queueData.ValidatorStatus = "Slashed" + } else { + queueData.ValidatorStatus = queueEntry.Validator.Status.String() + } + + if queueData.ShowUpcheck { + queueData.UpcheckActivity = uint8(services.GlobalBeaconService.GetValidatorLiveness(queueEntry.Validator.Index, 3)) + queueData.UpcheckMaximum = uint8(3) + } + + // Use the calculated EstimatedWithdrawalTime from the queue entry + queueData.EstimatedTime = chainState.SlotToTime(queueEntry.EstimatedWithdrawalTime) + + pageData.QueuedWithdrawals = append(pageData.QueuedWithdrawals, queueData) + } + pageData.QueuedTabCount = uint64(len(pageData.QueuedWithdrawals)) + } + + return pageData, 1 * time.Minute +} diff --git a/indexer/beacon/epochstats.go b/indexer/beacon/epochstats.go index 2eae9d63..c3f08494 100644 --- a/indexer/beacon/epochstats.go +++ b/indexer/beacon/epochstats.go @@ -56,8 +56,9 @@ type EpochStatsValues struct { ActiveBalance phase0.Gwei EffectiveBalance phase0.Gwei FirstDepositIndex uint64 - PendingWithdrawals []EpochStatsPendingWithdrawals + PendingWithdrawals []electra.PendingPartialWithdrawal PendingConsolidations []electra.PendingConsolidation + ConsolidatingBalance phase0.Gwei } // EpochStatsPacked holds the packed values for the epoch-specific information. @@ -73,8 +74,9 @@ type EpochStatsPacked struct { TotalBalance phase0.Gwei ActiveBalance phase0.Gwei FirstDepositIndex uint64 - PendingWithdrawals []EpochStatsPendingWithdrawals `ssz-max:"10000000"` - PendingConsolidations []electra.PendingConsolidation `ssz-max:"10000000"` + PendingWithdrawals []electra.PendingPartialWithdrawal `ssz-max:"10000000"` + PendingConsolidations []electra.PendingConsolidation `ssz-max:"10000000"` + ConsolidatingBalance phase0.Gwei } // EpochStatsPackedValidator holds the packed values for an active validator. @@ -83,11 +85,6 @@ type EpochStatsPackedValidator struct { EffectiveBalanceEth uint32 // effective balance in full ETH } -type EpochStatsPendingWithdrawals struct { - ValidatorIndex phase0.ValidatorIndex - Epoch phase0.Epoch -} - // newEpochStats creates a new EpochStats instance. func newEpochStats(epoch phase0.Epoch, dependentRoot phase0.Root) *EpochStats { stats := &EpochStats{ @@ -177,6 +174,7 @@ func (es *EpochStats) buildPackedSSZ() ([]byte, error) { FirstDepositIndex: es.values.FirstDepositIndex, PendingWithdrawals: es.values.PendingWithdrawals, PendingConsolidations: es.values.PendingConsolidations, + ConsolidatingBalance: es.values.ConsolidatingBalance, } lastValidatorIndex := phase0.ValidatorIndex(0) @@ -229,6 +227,7 @@ func (es *EpochStats) parsePackedSSZ(chainState *consensus.ChainState, ssz []byt FirstDepositIndex: packedValues.FirstDepositIndex, PendingWithdrawals: packedValues.PendingWithdrawals, PendingConsolidations: packedValues.PendingConsolidations, + ConsolidatingBalance: packedValues.ConsolidatingBalance, } lastValidatorIndex := phase0.ValidatorIndex(0) @@ -356,18 +355,21 @@ func (es *EpochStats) processState(indexer *Indexer, validatorSet []*phase0.Vali ActiveBalance: 0, EffectiveBalance: 0, FirstDepositIndex: dependentState.depositIndex, - PendingWithdrawals: make([]EpochStatsPendingWithdrawals, len(dependentState.pendingPartialWithdrawals)), + PendingWithdrawals: make([]electra.PendingPartialWithdrawal, len(dependentState.pendingPartialWithdrawals)), PendingConsolidations: make([]electra.PendingConsolidation, len(dependentState.pendingConsolidations)), } for i, pendingPartialWithdrawal := range dependentState.pendingPartialWithdrawals { - values.PendingWithdrawals[i] = EpochStatsPendingWithdrawals{ - ValidatorIndex: pendingPartialWithdrawal.ValidatorIndex, - Epoch: pendingPartialWithdrawal.WithdrawableEpoch, - } + values.PendingWithdrawals[i] = *pendingPartialWithdrawal } for i, pendingConsolidation := range dependentState.pendingConsolidations { + srcIndicee := pendingConsolidation.SourceIndex + srcValidator := validatorSet[srcIndicee] + if srcValidator != nil { + values.ConsolidatingBalance += srcValidator.EffectiveBalance + } + values.PendingConsolidations[i] = *pendingConsolidation } diff --git a/indexer/beacon/epochstats_ssz.go b/indexer/beacon/epochstats_ssz.go index 150661a8..c633886e 100644 --- a/indexer/beacon/epochstats_ssz.go +++ b/indexer/beacon/epochstats_ssz.go @@ -1,5 +1,5 @@ // Code generated by fastssz. DO NOT EDIT. -// Hash: d21f4066a02aa6a5565a5fe786eb8fce8d87f5d2cddac620b328c07fa0df1b95 +// Hash: 4b7503399e03b7a8a144430d3d096a73f059277a1f9ce163cea045f64762b0b6 // Version: 0.1.3 package beacon @@ -17,7 +17,7 @@ func (e *EpochStatsPacked) MarshalSSZ() ([]byte, error) { // MarshalSSZTo ssz marshals the EpochStatsPacked object to a target array func (e *EpochStatsPacked) MarshalSSZTo(buf []byte) (dst []byte, err error) { dst = buf - offset := int(108) + offset := int(116) // Offset (0) 'ActiveValidators' dst = ssz.WriteOffset(dst, offset) @@ -48,12 +48,15 @@ func (e *EpochStatsPacked) MarshalSSZTo(buf []byte) (dst []byte, err error) { // Offset (8) 'PendingWithdrawals' dst = ssz.WriteOffset(dst, offset) - offset += len(e.PendingWithdrawals) * 16 + offset += len(e.PendingWithdrawals) * 24 // Offset (9) 'PendingConsolidations' dst = ssz.WriteOffset(dst, offset) offset += len(e.PendingConsolidations) * 16 + // Field (10) 'ConsolidatingBalance' + dst = ssz.MarshalUint64(dst, uint64(e.ConsolidatingBalance)) + // Field (0) 'ActiveValidators' if size := len(e.ActiveValidators); size > 10000000 { err = ssz.ErrListTooBigFn("EpochStatsPacked.ActiveValidators", size, 10000000) @@ -112,7 +115,7 @@ func (e *EpochStatsPacked) MarshalSSZTo(buf []byte) (dst []byte, err error) { func (e *EpochStatsPacked) UnmarshalSSZ(buf []byte) error { var err error size := uint64(len(buf)) - if size < 108 { + if size < 116 { return ssz.ErrSize } @@ -124,7 +127,7 @@ func (e *EpochStatsPacked) UnmarshalSSZ(buf []byte) error { return ssz.ErrOffset } - if o0 < 108 { + if o0 < 116 { return ssz.ErrInvalidVariableOffset } @@ -163,6 +166,9 @@ func (e *EpochStatsPacked) UnmarshalSSZ(buf []byte) error { return ssz.ErrOffset } + // Field (10) 'ConsolidatingBalance' + e.ConsolidatingBalance = phase0.Gwei(ssz.UnmarshallUint64(buf[108:116])) + // Field (0) 'ActiveValidators' { buf = tail[o0:o1] @@ -207,13 +213,13 @@ func (e *EpochStatsPacked) UnmarshalSSZ(buf []byte) error { // Field (8) 'PendingWithdrawals' { buf = tail[o8:o9] - num, err := ssz.DivideInt2(len(buf), 16, 10000000) + num, err := ssz.DivideInt2(len(buf), 24, 10000000) if err != nil { return err } - e.PendingWithdrawals = make([]EpochStatsPendingWithdrawals, num) + e.PendingWithdrawals = make([]electra.PendingPartialWithdrawal, num) for ii := 0; ii < num; ii++ { - if err = e.PendingWithdrawals[ii].UnmarshalSSZ(buf[ii*16 : (ii+1)*16]); err != nil { + if err = e.PendingWithdrawals[ii].UnmarshalSSZ(buf[ii*24 : (ii+1)*24]); err != nil { return err } } @@ -238,7 +244,7 @@ func (e *EpochStatsPacked) UnmarshalSSZ(buf []byte) error { // SizeSSZ returns the ssz encoded size in bytes for the EpochStatsPacked object func (e *EpochStatsPacked) SizeSSZ() (size int) { - size = 108 + size = 116 // Field (0) 'ActiveValidators' size += len(e.ActiveValidators) * 8 @@ -250,7 +256,7 @@ func (e *EpochStatsPacked) SizeSSZ() (size int) { size += len(e.SyncCommitteeDuties) * 8 // Field (8) 'PendingWithdrawals' - size += len(e.PendingWithdrawals) * 16 + size += len(e.PendingWithdrawals) * 24 // Field (9) 'PendingConsolidations' size += len(e.PendingConsolidations) * 16 @@ -360,6 +366,9 @@ func (e *EpochStatsPacked) HashTreeRootWith(hh ssz.HashWalker) (err error) { hh.MerkleizeWithMixin(subIndx, num, 10000000) } + // Field (10) 'ConsolidatingBalance' + hh.PutUint64(uint64(e.ConsolidatingBalance)) + hh.Merkleize(indx) return } @@ -433,68 +442,3 @@ func (e *EpochStatsPackedValidator) HashTreeRootWith(hh ssz.HashWalker) (err err func (e *EpochStatsPackedValidator) GetTree() (*ssz.Node, error) { return ssz.ProofTree(e) } - -// MarshalSSZ ssz marshals the EpochStatsPendingWithdrawals object -func (e *EpochStatsPendingWithdrawals) MarshalSSZ() ([]byte, error) { - return ssz.MarshalSSZ(e) -} - -// MarshalSSZTo ssz marshals the EpochStatsPendingWithdrawals object to a target array -func (e *EpochStatsPendingWithdrawals) MarshalSSZTo(buf []byte) (dst []byte, err error) { - dst = buf - - // Field (0) 'ValidatorIndex' - dst = ssz.MarshalUint64(dst, uint64(e.ValidatorIndex)) - - // Field (1) 'Epoch' - dst = ssz.MarshalUint64(dst, uint64(e.Epoch)) - - return -} - -// UnmarshalSSZ ssz unmarshals the EpochStatsPendingWithdrawals object -func (e *EpochStatsPendingWithdrawals) UnmarshalSSZ(buf []byte) error { - var err error - size := uint64(len(buf)) - if size != 16 { - return ssz.ErrSize - } - - // Field (0) 'ValidatorIndex' - e.ValidatorIndex = phase0.ValidatorIndex(ssz.UnmarshallUint64(buf[0:8])) - - // Field (1) 'Epoch' - e.Epoch = phase0.Epoch(ssz.UnmarshallUint64(buf[8:16])) - - return err -} - -// SizeSSZ returns the ssz encoded size in bytes for the EpochStatsPendingWithdrawals object -func (e *EpochStatsPendingWithdrawals) SizeSSZ() (size int) { - size = 16 - return -} - -// HashTreeRoot ssz hashes the EpochStatsPendingWithdrawals object -func (e *EpochStatsPendingWithdrawals) HashTreeRoot() ([32]byte, error) { - return ssz.HashWithDefaultHasher(e) -} - -// HashTreeRootWith ssz hashes the EpochStatsPendingWithdrawals object with a hasher -func (e *EpochStatsPendingWithdrawals) HashTreeRootWith(hh ssz.HashWalker) (err error) { - indx := hh.Index() - - // Field (0) 'ValidatorIndex' - hh.PutUint64(uint64(e.ValidatorIndex)) - - // Field (1) 'Epoch' - hh.PutUint64(uint64(e.Epoch)) - - hh.Merkleize(indx) - return -} - -// GetTree ssz hashes the EpochStatsPendingWithdrawals object -func (e *EpochStatsPendingWithdrawals) GetTree() (*ssz.Node, error) { - return ssz.ProofTree(e) -} diff --git a/indexer/beacon/state_sim.go b/indexer/beacon/state_sim.go index 2dc55730..8480c247 100644 --- a/indexer/beacon/state_sim.go +++ b/indexer/beacon/state_sim.go @@ -21,7 +21,7 @@ type stateSimulator struct { type stateSimulatorState struct { epochRoot phase0.Root block *Block - pendingWithdrawals []EpochStatsPendingWithdrawals + pendingWithdrawals []electra.PendingPartialWithdrawal additionalWithdrawals []phase0.ValidatorIndex pendingConsolidationCount uint64 validatorMap map[phase0.ValidatorIndex]*phase0.Validator @@ -75,7 +75,7 @@ func (sim *stateSimulator) getParentBlocks(block *Block) []*Block { func (sim *stateSimulator) resetState(block *Block) *stateSimulatorState { pendingWithdrawals := sim.epochStatsValues.PendingWithdrawals if pendingWithdrawals == nil { - pendingWithdrawals = []EpochStatsPendingWithdrawals{} + pendingWithdrawals = []electra.PendingPartialWithdrawal{} } epochRoot := block.Root @@ -343,7 +343,7 @@ func (sim *stateSimulator) applyBlock(block *Block) [][]uint8 { processedWithdrawals := uint64(0) skippedWithdrawals := uint64(0) for _, pendingWithdrawal := range sim.prevState.pendingWithdrawals { - if pendingWithdrawal.Epoch > sim.epochStats.epoch { + if pendingWithdrawal.WithdrawableEpoch > sim.epochStats.epoch { break } diff --git a/services/chainservice_consolidations.go b/services/chainservice_consolidations.go index 4a2861b6..d4c32b11 100644 --- a/services/chainservice_consolidations.go +++ b/services/chainservice_consolidations.go @@ -4,12 +4,15 @@ import ( "bytes" "strings" + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethpandaops/dora/db" "github.com/ethpandaops/dora/dbtypes" "github.com/ethpandaops/dora/indexer/beacon" "github.com/prysmaticlabs/prysm/v5/container/slice" "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" ) type CombinedConsolidationRequest struct { @@ -316,3 +319,117 @@ func (bs *ChainService) GetConsolidationRequestOperationsByFilter(filter *dbtype return resObjs, cachedMatchesLen + dbCount } + +type ConsolidationQueueEntry struct { + QueuePos uint64 + SrcIndex phase0.ValidatorIndex + TgtIndex phase0.ValidatorIndex + SrcValidator *v1.Validator + TgtValidator *v1.Validator + SrcValidatorName string + TgtValidatorName string +} + +type ConsolidationQueueFilter struct { + MinSrcIndex *uint64 + MaxSrcIndex *uint64 + MinTgtIndex *uint64 + MaxTgtIndex *uint64 + PublicKey []byte + ValidatorName string + ReverseOrder bool +} + +func (bs *ChainService) GetConsolidationQueueByFilter(filter *ConsolidationQueueFilter, offset uint64, limit uint64) ([]*ConsolidationQueueEntry, uint64) { + epochStats, _ := bs.GetRecentEpochStats(nil) + if epochStats == nil { + return nil, 0 + } + + var filterIndex *phase0.ValidatorIndex + if len(filter.PublicKey) > 0 { + if index, found := bs.beaconIndexer.GetValidatorIndexByPubkey(phase0.BLSPubKey(filter.PublicKey)); found { + filterIndex = &index + } + } + + pendingConsolidations := epochStats.PendingConsolidations + if filter.ReverseOrder { + revConsolidations := make([]electra.PendingConsolidation, len(pendingConsolidations)) + for idx, consolidation := range pendingConsolidations { + revConsolidations[len(pendingConsolidations)-idx-1] = consolidation + } + pendingConsolidations = revConsolidations + } + + queue := []*ConsolidationQueueEntry{} + validatorIndexesMap := map[phase0.ValidatorIndex]bool{} + for idx, consolidation := range pendingConsolidations { + if filter.MinSrcIndex != nil && consolidation.SourceIndex < phase0.ValidatorIndex(*filter.MinSrcIndex) { + continue + } + if filter.MaxSrcIndex != nil && consolidation.SourceIndex > phase0.ValidatorIndex(*filter.MaxSrcIndex) { + continue + } + if filter.MinTgtIndex != nil && consolidation.TargetIndex < phase0.ValidatorIndex(*filter.MinTgtIndex) { + continue + } + if filter.MaxTgtIndex != nil && consolidation.TargetIndex > phase0.ValidatorIndex(*filter.MaxTgtIndex) { + continue + } + if filterIndex != nil && consolidation.SourceIndex != *filterIndex && consolidation.TargetIndex != *filterIndex { + continue + } + + srcName := bs.validatorNames.GetValidatorName(uint64(consolidation.SourceIndex)) + tgtName := bs.validatorNames.GetValidatorName(uint64(consolidation.TargetIndex)) + + if filter.ValidatorName != "" && !strings.Contains(srcName, filter.ValidatorName) && !strings.Contains(tgtName, filter.ValidatorName) { + continue + } + + validatorIndexesMap[consolidation.SourceIndex] = true + validatorIndexesMap[consolidation.TargetIndex] = true + + queue = append(queue, &ConsolidationQueueEntry{ + QueuePos: uint64(idx), + SrcIndex: consolidation.SourceIndex, + TgtIndex: consolidation.TargetIndex, + SrcValidatorName: srcName, + TgtValidatorName: tgtName, + }) + + if len(queue) > int(offset+limit) { + break + } + } + + validatorIndexes := maps.Keys(validatorIndexesMap) + if len(validatorIndexes) == 0 { + return []*ConsolidationQueueEntry{}, 0 + } + + validators, _ := bs.GetFilteredValidatorSet(&dbtypes.ValidatorFilter{ + Indices: validatorIndexes, + }, false) + + validatorsMap := map[phase0.ValidatorIndex]*v1.Validator{} + for idx, validator := range validators { + validatorsMap[validator.Index] = &validators[idx] + } + + for _, entry := range queue { + entry.SrcValidator = validatorsMap[entry.SrcIndex] + entry.TgtValidator = validatorsMap[entry.TgtIndex] + } + + if len(queue) < int(offset) { + return []*ConsolidationQueueEntry{}, uint64(len(queue)) + } + + if len(queue) > int(offset+limit) { + return queue[offset : offset+limit], uint64(len(queue)) + } + + return queue[offset:], uint64(len(queue)) +} diff --git a/services/chainservice_objects.go b/services/chainservice_objects.go index 8afdae2d..3e953d88 100644 --- a/services/chainservice_objects.go +++ b/services/chainservice_objects.go @@ -82,13 +82,17 @@ func (bs *ChainService) GetVoluntaryExitsByFilter(filter *dbtypes.VoluntaryExitF // load older objects from db dbPage := pageIdx - cachedPages + if cachedPages > pageIdx { + dbPage = 0 + } + dbCacheOffset := uint64(pageSize) - (cachedMatchesLen % uint64(pageSize)) var dbObjects []*dbtypes.VoluntaryExit var dbCount uint64 var err error - if resIdx > int(pageSize) { + if resIdx >= int(pageSize) { // all results from cache, just get result count from db _, dbCount, err = db.GetVoluntaryExitsFiltered(0, 1, uint64(finalizedBlock), filter) } else if dbPage == 0 { diff --git a/services/chainservice_withdrawals.go b/services/chainservice_withdrawals.go index fc482014..96c493e1 100644 --- a/services/chainservice_withdrawals.go +++ b/services/chainservice_withdrawals.go @@ -4,6 +4,7 @@ import ( "bytes" "strings" + v1 "github.com/attestantio/go-eth2-client/api/v1" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethpandaops/dora/db" "github.com/ethpandaops/dora/dbtypes" @@ -286,3 +287,136 @@ func (bs *ChainService) GetWithdrawalRequestOperationsByFilter(filter *dbtypes.W return resObjs, cachedMatchesLen + dbCount } + +type WithdrawalQueueEntry struct { + ValidatorIndex phase0.ValidatorIndex + Validator *v1.Validator + ValidatorName string + WithdrawableEpoch phase0.Epoch + Amount phase0.Gwei + EstimatedWithdrawalTime phase0.Slot +} + +type WithdrawalQueueFilter struct { + MinValidatorIndex *uint64 + MaxValidatorIndex *uint64 + ValidatorName string + PublicKey []byte +} + +func (bs *ChainService) GetWithdrawalQueueByFilter(filter *WithdrawalQueueFilter, pageOffset uint64, pageSize uint32) ([]*WithdrawalQueueEntry, uint64, phase0.Gwei) { + chainState := bs.consensusPool.GetChainState() + epochStats, epochStatsEpoch := bs.GetRecentEpochStats(nil) + + if epochStats == nil { + return []*WithdrawalQueueEntry{}, 0, 0 + } + + var filterIndex *phase0.ValidatorIndex + if len(filter.PublicKey) > 0 { + if index, found := bs.beaconIndexer.GetValidatorIndexByPubkey(phase0.BLSPubKey(filter.PublicKey)); found { + filterIndex = &index + } + } + + // First pass - collect all matching entries and validator indices + queue := []*WithdrawalQueueEntry{} + validatorIndexesMap := map[phase0.ValidatorIndex]bool{} + totalAmount := phase0.Gwei(0) + + for _, pendingWithdrawal := range epochStats.PendingWithdrawals { + // Apply filters + if filter.MinValidatorIndex != nil && uint64(pendingWithdrawal.ValidatorIndex) < *filter.MinValidatorIndex { + continue + } + if filter.MaxValidatorIndex != nil && uint64(pendingWithdrawal.ValidatorIndex) > *filter.MaxValidatorIndex { + continue + } + if filterIndex != nil && pendingWithdrawal.ValidatorIndex != *filterIndex { + continue + } + if filter.ValidatorName != "" { + validatorName := bs.validatorNames.GetValidatorName(uint64(pendingWithdrawal.ValidatorIndex)) + if !strings.Contains(validatorName, filter.ValidatorName) { + continue + } + } + + validatorIndexesMap[pendingWithdrawal.ValidatorIndex] = true + totalAmount += pendingWithdrawal.Amount + + queue = append(queue, &WithdrawalQueueEntry{ + ValidatorIndex: pendingWithdrawal.ValidatorIndex, + ValidatorName: bs.validatorNames.GetValidatorName(uint64(pendingWithdrawal.ValidatorIndex)), + WithdrawableEpoch: pendingWithdrawal.WithdrawableEpoch, + Amount: pendingWithdrawal.Amount, + }) + } + + validatorIndexes := make([]phase0.ValidatorIndex, 0, len(validatorIndexesMap)) + for index := range validatorIndexesMap { + validatorIndexes = append(validatorIndexes, index) + } + + if len(validatorIndexes) == 0 { + return []*WithdrawalQueueEntry{}, 0, 0 + } + + // Bulk load all validators + validators, _ := bs.GetFilteredValidatorSet(&dbtypes.ValidatorFilter{ + Indices: validatorIndexes, + }, false) + + validatorsMap := map[phase0.ValidatorIndex]*v1.Validator{} + for idx, validator := range validators { + validatorsMap[validator.Index] = &validators[idx] + } + + // Second pass - populate validator data and calculate withdrawal times + specs := chainState.GetSpecs() + currentSlot := chainState.EpochToSlot(epochStatsEpoch) + processableWithdrawals := uint64(0) + processableSlot := phase0.Slot(0) + + for _, entry := range queue { + entry.Validator = validatorsMap[entry.ValidatorIndex] + + // Calculate estimated withdrawal time + minWithdrawalSlot := chainState.EpochToSlot(entry.WithdrawableEpoch) + if minWithdrawalSlot < currentSlot { + minWithdrawalSlot = currentSlot + } + if processableSlot < minWithdrawalSlot { + withdrawnCount := uint64(minWithdrawalSlot-processableSlot) * specs.MaxPendingPartialsPerWithdrawalsSweep + if withdrawnCount >= processableWithdrawals { + processableWithdrawals = 0 + processableSlot = minWithdrawalSlot + } else { + processableWithdrawals -= withdrawnCount + processableSlot = minWithdrawalSlot + } + } + + if entry.Validator != nil && entry.Validator.Status == v1.ValidatorStateActiveOngoing { + processableWithdrawals++ + } + + entry.EstimatedWithdrawalTime = minWithdrawalSlot + phase0.Slot(processableWithdrawals/specs.MaxPendingPartialsPerWithdrawalsSweep) + } + + totalCount := uint64(len(queue)) + + // Apply pagination + start := pageOffset + end := pageOffset + uint64(pageSize) + + if start >= totalCount { + return []*WithdrawalQueueEntry{}, totalCount, totalAmount + } + + if end > totalCount { + end = totalCount + } + + return queue[start:end], totalCount, totalAmount +} diff --git a/templates/consolidations/consolidations.html b/templates/consolidations/consolidations.html new file mode 100644 index 00000000..c5b95def --- /dev/null +++ b/templates/consolidations/consolidations.html @@ -0,0 +1,380 @@ +{{ define "page" }} +
| Slot | +Time | +Source Address | +Source Validator | +Target Validator | +Transaction | +Incl. Status | +
|---|---|---|---|---|---|---|
| + {{ if not $consolidation.IsIncluded }} + ? + {{ else if eq $consolidation.Status 2 }} + {{ formatAddCommas $consolidation.SlotNumber }} + {{ else }} + {{ formatAddCommas $consolidation.SlotNumber }} + {{ end }} + | +{{ formatRecentTimeShort $consolidation.Time }} | +
+
+
+ {{ if $consolidation.SourceAddr }}
+ {{ ethAddressLink $consolidation.SourceAddr }}
+ {{ else }}
+ ?
+ {{ end }}
+
+ {{ if $consolidation.SourceAddr }}
+
+
+
+
+ {{ end }}
+ |
+
+ {{- if $consolidation.SourceValidatorValid }}
+ {{ formatValidatorWithIndex $consolidation.SourceValidatorIndex $consolidation.SourceValidatorName }}
+ {{- else }}
+
+ 0x{{ printf "%x" $consolidation.SourcePublicKey }}
+
+ {{- end }}
+
+
+
+ |
+
+ {{- if $consolidation.TargetValidatorValid }}
+ {{ formatValidatorWithIndex $consolidation.TargetValidatorIndex $consolidation.TargetValidatorName }}
+ {{- else }}
+
+ 0x{{ printf "%x" $consolidation.TargetPublicKey }}
+
+ {{- end }}
+
+
+
+ |
+
+ {{- if $consolidation.LinkedTransaction }}
+
+ {{ ethTransactionLink $consolidation.TransactionHash 0 }}
+
+ {{- else }}
+ ?
+ {{- end }}
+
+
+
+ |
+ + {{ if eq $consolidation.TxStatus 1 }} + Tx Included + {{ else if eq $consolidation.TxStatus 2 }} + Tx Orphaned + {{ end }} + + {{ if eq $consolidation.Status 0 }} + Req. Pending + {{ else if eq $consolidation.Status 1 }} + {{ if eq $consolidation.Result 0 }} + Req. Included + {{ else if eq $consolidation.Result 1 }} + Req. Processed + {{ else }} + Req. Failed + {{ end }} + {{ else if eq $consolidation.Status 2 }} + Req. Orphaned + {{ end }} + | +
| + | + + | ++ | ||||
| Source Validator | +Target Validator | +Balance | +Source Status | +Estimated Time | +
|---|---|---|---|---|
|
+ {{- if $consolidation.SourceValidatorExists }}
+ {{ formatValidatorWithIndex $consolidation.SourceValidatorIndex $consolidation.SourceValidatorName }}
+ {{- else }}
+
+ 0x{{ printf "%x" $consolidation.SourcePublicKey }}
+
+ {{- end }}
+
+
+
+ |
+
+ {{- if $consolidation.TargetValidatorExists }}
+ {{ formatValidatorWithIndex $consolidation.TargetValidatorIndex $consolidation.TargetValidatorName }}
+ {{- else }}
+
+ 0x{{ printf "%x" $consolidation.TargetPublicKey }}
+
+ {{- end }}
+
+
+
+ |
+ + {{ if $consolidation.SourceValidatorExists }} + {{ formatFullEthFromGwei $consolidation.SourceEffectiveBalance }} + {{ else }} + - + {{ end }} + | ++ {{- $consolidation.SourceValidatorStatus -}} + {{- if $consolidation.ShowUpcheck -}} + {{- if eq $consolidation.UpcheckActivity $consolidation.UpcheckMaximum }} + + {{- else if gt $consolidation.UpcheckActivity 0 }} + + {{- else }} + + {{- end -}} + {{- end -}} + | ++ {{ if $consolidation.EstimatedTime.IsZero }} + - + {{ else }} + + {{ formatRecentTimeShort $consolidation.EstimatedTime }} + + {{ end }} + | +
| + + | +||||
| Slot | +Time | +Validator | +Public Key | +Status | +
|---|---|---|---|---|
| + {{ if $exit.Orphaned }} + {{ formatAddCommas $exit.SlotNumber }} + {{ else }} + {{ formatAddCommas $exit.SlotNumber }} + {{ end }} + | +{{ formatRecentTimeShort $exit.Time }} | ++ {{ formatValidatorWithIndex $exit.ValidatorIndex $exit.ValidatorName }} + | +
+
+ 0x{{ printf "%x" $exit.PublicKey }}
+
+
+
+
+ |
+ + {{ if $exit.Orphaned }} + Orphaned + {{ end }} + {{- $exit.ValidatorStatus -}} + {{- if $exit.ShowUpcheck -}} + {{- if eq $exit.UpcheckActivity $exit.UpcheckMaximum }} + + {{- else if gt $exit.UpcheckActivity 0 }} + + {{- else }} + + {{- end -}} + {{- end -}} + | +
| + | + + | ++ | ||
| Validator | +Balance | +Exit Epoch | +Status | +Estimated Time | +
|---|---|---|---|---|
| + {{ formatValidatorWithIndex $validator.ValidatorIndex $validator.ValidatorName }} + | ++ {{ formatFullEthFromGwei $validator.EffectiveBalance }} + | ++ {{ formatAddCommas $validator.ExitEpoch }} + | ++ {{- $validator.ValidatorStatus -}} + {{- if $validator.ShowUpcheck -}} + {{- if eq $validator.UpcheckActivity $validator.UpcheckMaximum }} + + {{- else if gt $validator.UpcheckActivity 0 }} + + {{- else }} + + {{- end -}} + {{- end -}} + | ++ {{ if $validator.EstimatedTime.IsZero }} + - + {{ else }} + + {{ formatRecentTimeShort $validator.EstimatedTime }} + + {{ end }} + | +
| + + | +||||
| Source Validator | +Target Validator | +Balance | +Source Status | +Estimated Time | +
|---|---|---|---|---|
|
+ {{- if $consolidation.SourceValidatorExists }}
+ {{ formatValidatorWithIndex $consolidation.SourceValidatorIndex $consolidation.SourceValidatorName }}
+ {{- else }}
+
+ 0x{{ printf "%x" $consolidation.SourcePublicKey }}
+
+ {{- end }}
+
+
+
+ |
+
+ {{- if $consolidation.TargetValidatorExists }}
+ {{ formatValidatorWithIndex $consolidation.TargetValidatorIndex $consolidation.TargetValidatorName }}
+ {{- else }}
+
+ 0x{{ printf "%x" $consolidation.TargetPublicKey }}
+
+ {{- end }}
+
+
+
+ |
+ + {{ if $consolidation.SourceValidatorExists }} + {{ formatFullEthFromGwei $consolidation.SourceEffectiveBalance }} + {{ else }} + - + {{ end }} + | ++ {{- $consolidation.SourceValidatorStatus -}} + {{- if $consolidation.ShowUpcheck -}} + {{- if eq $consolidation.UpcheckActivity $consolidation.UpcheckMaximum }} + + {{- else if gt $consolidation.UpcheckActivity 0 }} + + {{- else }} + + {{- end -}} + {{- end -}} + | ++ {{ if $consolidation.EstimatedTime.IsZero }} + - + {{ else }} + + {{ formatRecentTimeShort $consolidation.EstimatedTime }} + + {{ end }} + | +
| + + | +||||
| Validator | +Amount | +Status | +Estimated Time | +
|---|---|---|---|
| + {{ formatValidatorWithIndex $withdrawal.ValidatorIndex $withdrawal.ValidatorName }} + | ++ {{ formatFullEthFromGwei $withdrawal.Amount }} + | ++ {{- $withdrawal.ValidatorStatus -}} + {{- if $withdrawal.ShowUpcheck -}} + {{- if eq $withdrawal.UpcheckActivity $withdrawal.UpcheckMaximum }} + + {{- else if gt $withdrawal.UpcheckActivity 0 }} + + {{- else }} + + {{- end -}} + {{- end -}} + | ++ {{ if $withdrawal.EstimatedTime.IsZero }} + - + {{ else }} + + {{ formatRecentTimeShort $withdrawal.EstimatedTime }} + + {{ end }} + | +
| + + | +|||
| Slot | +Time | +Source Address | +Req. Type | +Validator | +Amount | +Transaction | +Incl. Status | +
|---|---|---|---|---|---|---|---|
| + {{ if not $withdrawal.IsIncluded }} + ? + {{ else }} + {{ formatAddCommas $withdrawal.SlotNumber }} + {{ end }} + | +{{ formatRecentTimeShort $withdrawal.Time }} | +
+
+
+ {{ if $withdrawal.SourceAddr }}
+ {{ ethAddressLink $withdrawal.SourceAddr }}
+ {{ else }}
+ ?
+ {{ end }}
+
+ {{ if $withdrawal.SourceAddr }}
+
+
+
+
+ {{ end }}
+ |
+ + {{- if eq $withdrawal.Amount 0 }} + Exit + {{- else }} + Withdrawal + {{- end }} + | +
+ {{- if $withdrawal.ValidatorValid }}
+ {{ formatValidatorWithIndex $withdrawal.ValidatorIndex $withdrawal.ValidatorName }}
+ {{- else }}
+
+ 0x{{ printf "%x" $withdrawal.PublicKey }}
+
+ {{- end }}
+
+
+
+ |
+ {{ formatFullEthFromGwei $withdrawal.Amount }} | +
+ {{- if $withdrawal.LinkedTransaction }}
+
+ {{ ethTransactionLink $withdrawal.TransactionHash 0 }}
+
+ {{- else }}
+ ?
+ {{- end }}
+
+
+
+ |
+ + {{ if eq $withdrawal.TxStatus 1 }} + Tx Included + {{ else if eq $withdrawal.TxStatus 2 }} + Tx Orphaned + {{ end }} + + {{ if eq $withdrawal.Status 0 }} + Req. Pending + {{ else if eq $withdrawal.Status 1 }} + {{ if eq $withdrawal.Result 0 }} + Req. Included + {{ else if eq $withdrawal.Result 1 }} + Req. Processed + {{ else }} + Req. Failed + {{ end }} + {{ else if eq $withdrawal.Status 2 }} + Req. Orphaned + {{ end }} + | +
| + | + + | ++ | |||||
| Validator | +Amount | +Status | +Estimated Time | +
|---|---|---|---|
| + {{ formatValidatorWithIndex $withdrawal.ValidatorIndex $withdrawal.ValidatorName }} + | ++ {{ formatFullEthFromGwei $withdrawal.Amount }} + | ++ {{- $withdrawal.ValidatorStatus -}} + {{- if $withdrawal.ShowUpcheck -}} + {{- if eq $withdrawal.UpcheckActivity $withdrawal.UpcheckMaximum }} + + {{- else if gt $withdrawal.UpcheckActivity 0 }} + + {{- else }} + + {{- end -}} + {{- end -}} + | ++ {{ if $withdrawal.EstimatedTime.IsZero }} + - + {{ else }} + + {{ formatRecentTimeShort $withdrawal.EstimatedTime }} + + {{ end }} + | +
| + + | +|||