diff --git a/cmd/dora-explorer/main.go b/cmd/dora-explorer/main.go index fe831ca8..cc3825e0 100644 --- a/cmd/dora-explorer/main.go +++ b/cmd/dora-explorer/main.go @@ -97,6 +97,8 @@ func startFrontend() { router.HandleFunc("/validators/deposits", handlers.Deposits).Methods("GET") router.HandleFunc("/validators/initiated_deposits", handlers.InitiatedDeposits).Methods("GET") 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("/validator/{idxOrPubKey}", handlers.Validator).Methods("GET") router.HandleFunc("/validator/{index}/slots", handlers.ValidatorSlots).Methods("GET") diff --git a/db/schema/pgsql/20240506203555_exits-and-slashings.sql b/db/schema/pgsql/20240506203555_exits-and-slashings.sql new file mode 100644 index 00000000..b19d84cf --- /dev/null +++ b/db/schema/pgsql/20240506203555_exits-and-slashings.sql @@ -0,0 +1,55 @@ +-- +goose Up +-- +goose StatementBegin + +CREATE TABLE IF NOT EXISTS voluntary_exits ( + slot_number INT NOT NULL, + slot_index INT NOT NULL, + slot_root bytea NOT NULL, + orphaned bool NOT NULL DEFAULT FALSE, + validator BIGINT NOT NULL, + CONSTRAINT voluntary_exits_pkey PRIMARY KEY (slot_root, slot_index) +); + +CREATE INDEX IF NOT EXISTS "voluntary_exits_validator_idx" + ON public."voluntary_exits" + ("validator" ASC NULLS FIRST); + +CREATE INDEX IF NOT EXISTS "voluntary_exits_slot_number_idx" + ON public."voluntary_exits" + ("slot_number" ASC NULLS FIRST); + +CREATE TABLE IF NOT EXISTS slashings ( + slot_number INT NOT NULL, + slot_index INT NOT NULL, + slot_root bytea NOT NULL, + orphaned bool NOT NULL DEFAULT FALSE, + validator BIGINT NOT NULL, + slasher BIGINT NOT NULL, + reason INT NOT NULL, + CONSTRAINT slashings_pkey PRIMARY KEY (slot_root, slot_index, validator) +); + +CREATE INDEX IF NOT EXISTS "slashings_slot_number_idx" + ON public."slashings" + ("slot_number" ASC NULLS FIRST); + +CREATE INDEX IF NOT EXISTS "slashings_reason_slot_number_idx" + ON public."slashings" + ( + "reason" ASC NULLS FIRST, + "slot_number" ASC NULLS FIRST + ); + +CREATE INDEX IF NOT EXISTS "slashings_validator_idx" + ON public."slashings" + ("validator" ASC NULLS FIRST); + +CREATE INDEX IF NOT EXISTS "slashings_slasher_idx" + ON public."slashings" + ("slasher" ASC NULLS FIRST); + +-- +goose StatementEnd +-- +goose Down +-- +goose StatementBegin +SELECT 'NOT SUPPORTED'; +-- +goose StatementEnd diff --git a/db/schema/sqlite/20240506203555_exits-and-slashings.sql b/db/schema/sqlite/20240506203555_exits-and-slashings.sql new file mode 100644 index 00000000..946acd9c --- /dev/null +++ b/db/schema/sqlite/20240506203555_exits-and-slashings.sql @@ -0,0 +1,55 @@ +-- +goose Up +-- +goose StatementBegin + +CREATE TABLE IF NOT EXISTS voluntary_exits ( + slot_number INT NOT NULL, + slot_index INT NOT NULL, + slot_root BLOB NOT NULL, + orphaned bool NOT NULL DEFAULT FALSE, + validator BIGINT NOT NULL, + CONSTRAINT voluntary_exits_pkey PRIMARY KEY (slot_root, slot_index) +); + +CREATE INDEX IF NOT EXISTS "voluntary_exits_validator_idx" + ON "voluntary_exits" + ("validator" ASC); + +CREATE INDEX IF NOT EXISTS "voluntary_exits_slot_number_idx" + ON "voluntary_exits" + ("slot_number" ASC); + +CREATE TABLE IF NOT EXISTS slashings ( + slot_number INT NOT NULL, + slot_index INT NOT NULL, + slot_root BLOB NOT NULL, + orphaned bool NOT NULL DEFAULT FALSE, + validator BIGINT NOT NULL, + slasher BIGINT NOT NULL, + reason INT NOT NULL, + CONSTRAINT slashings_pkey PRIMARY KEY (slot_root, slot_index, validator) +); + +CREATE INDEX IF NOT EXISTS "slashings_slot_number_idx" + ON "slashings" + ("slot_number" ASC); + +CREATE INDEX IF NOT EXISTS "slashings_reason_slot_number_idx" + ON "slashings" + ( + "reason" ASC, + "slot_number" ASC + ); + +CREATE INDEX IF NOT EXISTS "slashings_validator_idx" + ON "slashings" + ("validator" ASC); + +CREATE INDEX IF NOT EXISTS "slashings_slasher_idx" + ON "slashings" + ("slasher" ASC); + +-- +goose StatementEnd +-- +goose Down +-- +goose StatementBegin +SELECT 'NOT SUPPORTED'; +-- +goose StatementEnd diff --git a/db/slashings.go b/db/slashings.go new file mode 100644 index 00000000..c4791a14 --- /dev/null +++ b/db/slashings.go @@ -0,0 +1,223 @@ +package db + +import ( + "fmt" + "strings" + + "github.com/ethpandaops/dora/dbtypes" + "github.com/jmoiron/sqlx" +) + +func InsertSlashings(slashings []*dbtypes.Slashing, tx *sqlx.Tx) error { + var sql strings.Builder + fmt.Fprint(&sql, + EngineQuery(map[dbtypes.DBEngineType]string{ + dbtypes.DBEnginePgsql: "INSERT INTO slashings ", + dbtypes.DBEngineSqlite: "INSERT OR REPLACE INTO slashings ", + }), + "(slot_number, slot_index, slot_root, orphaned, validator, slasher, reason)", + " VALUES ", + ) + argIdx := 0 + fieldCount := 7 + + args := make([]any, len(slashings)*fieldCount) + for i, slashing := range slashings { + if i > 0 { + fmt.Fprintf(&sql, ", ") + } + fmt.Fprintf(&sql, "(") + for f := 0; f < fieldCount; f++ { + if f > 0 { + fmt.Fprintf(&sql, ", ") + } + fmt.Fprintf(&sql, "$%v", argIdx+f+1) + + } + fmt.Fprintf(&sql, ")") + + args[argIdx+0] = slashing.SlotNumber + args[argIdx+1] = slashing.SlotIndex + args[argIdx+2] = slashing.SlotRoot + args[argIdx+3] = slashing.Orphaned + args[argIdx+4] = slashing.ValidatorIndex + args[argIdx+5] = slashing.SlasherIndex + args[argIdx+6] = slashing.Reason + argIdx += fieldCount + } + fmt.Fprint(&sql, EngineQuery(map[dbtypes.DBEngineType]string{ + dbtypes.DBEnginePgsql: " ON CONFLICT (slot_root, slot_index, validator) DO UPDATE SET orphaned = excluded.orphaned", + dbtypes.DBEngineSqlite: "", + })) + + _, err := tx.Exec(sql.String(), args...) + if err != nil { + return err + } + return nil +} + +func GetSlashings(firstSlot uint64, limit uint32, reason dbtypes.SlashingReason) []*dbtypes.Slashing { + var sql strings.Builder + args := []any{} + fmt.Fprint(&sql, ` + SELECT + slot_number, slot_index, slot_root, orphaned, validator, slasher, reason + FROM slashings + `) + filterOp := "WHERE" + if firstSlot > 0 { + args = append(args, firstSlot) + fmt.Fprintf(&sql, " %v slot_number <= $%v ", filterOp, len(args)) + filterOp = "AND" + } + if reason > 0 { + args = append(args, reason) + fmt.Fprintf(&sql, " %v reason <= $%v ", filterOp, len(args)) + filterOp = "AND" + } + + args = append(args, limit) + fmt.Fprintf(&sql, ` + ORDER BY slot_number DESC, slot_index DESC + LIMIT $%v + `, len(args)) + + slashings := []*dbtypes.Slashing{} + err := ReaderDb.Select(&slashings, sql.String(), args...) + if err != nil { + logger.Errorf("Error while fetching slashings: %v", err) + return nil + } + return slashings +} + +func GetSlashingForValidator(validator uint64) *dbtypes.Slashing { + var sql strings.Builder + args := []any{ + validator, + } + fmt.Fprint(&sql, ` + SELECT + slot_number, slot_index, slot_root, orphaned, validator, slasher, reason + FROM slashings + WHERE validator = $1 + `) + + slashing := &dbtypes.Slashing{} + err := ReaderDb.Get(&slashing, sql.String(), args...) + if err != nil { + return nil + } + return slashing +} + +func GetSlashingsFiltered(offset uint64, limit uint32, finalizedBlock uint64, filter *dbtypes.SlashingFilter) ([]*dbtypes.Slashing, uint64, error) { + var sql strings.Builder + args := []any{} + fmt.Fprint(&sql, ` + WITH cte AS ( + SELECT + slot_number, slot_index, slot_root, orphaned, validator, slasher, reason + FROM slashings + `) + + if filter.ValidatorName != "" { + fmt.Fprint(&sql, ` + LEFT JOIN validator_names ON validator_names."index" = slashings.validator + `) + } + if filter.SlasherName != "" { + fmt.Fprint(&sql, ` + LEFT JOIN validator_names AS slasher_names ON slasher_names."index" = slashings.slasher + `) + } + + 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 filter.MinIndex > 0 { + args = append(args, filter.MinIndex) + fmt.Fprintf(&sql, " %v validator >= $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.MaxIndex > 0 { + args = append(args, filter.MaxIndex) + fmt.Fprintf(&sql, " %v validator <= $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.WithReason > 0 { + args = append(args, filter.WithReason) + fmt.Fprintf(&sql, " %v reason = $%v", filterOp, 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 AND orphaned = true)", filterOp, len(args)) + filterOp = "AND" + } + if filter.ValidatorName != "" { + args = append(args, "%"+filter.ValidatorName+"%") + fmt.Fprintf(&sql, " %v ", filterOp) + fmt.Fprintf(&sql, EngineQuery(map[dbtypes.DBEngineType]string{ + dbtypes.DBEnginePgsql: ` validator_names.name ilike $%v `, + dbtypes.DBEngineSqlite: ` validator_names.name LIKE $%v `, + }), len(args)) + + filterOp = "AND" + } + if filter.SlasherName != "" { + args = append(args, "%"+filter.SlasherName+"%") + fmt.Fprintf(&sql, " %v ", filterOp) + fmt.Fprintf(&sql, EngineQuery(map[dbtypes.DBEngineType]string{ + dbtypes.DBEnginePgsql: ` slasher_names.name ilike $%v `, + dbtypes.DBEngineSqlite: ` slasher_names.name LIKE $%v `, + }), len(args)) + + filterOp = "AND" + } + + args = append(args, limit) + fmt.Fprintf(&sql, `) + SELECT + count(*) AS slot_number, + 0 AS slot_index, + null AS slot_root, + false AS orphaned, + 0 AS validator, + 0 AS slasher, + 0 AS reason + 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") + + slashings := []*dbtypes.Slashing{} + err := ReaderDb.Select(&slashings, sql.String(), args...) + if err != nil { + logger.Errorf("Error while fetching filtered slashings: %v", err) + return nil, 0, err + } + + return slashings[1:], slashings[0].SlotNumber, nil +} diff --git a/db/voluntary_exits.go b/db/voluntary_exits.go new file mode 100644 index 00000000..4c907664 --- /dev/null +++ b/db/voluntary_exits.go @@ -0,0 +1,192 @@ +package db + +import ( + "fmt" + "strings" + + "github.com/ethpandaops/dora/dbtypes" + "github.com/jmoiron/sqlx" +) + +func InsertVoluntaryExits(voluntaryExits []*dbtypes.VoluntaryExit, tx *sqlx.Tx) error { + var sql strings.Builder + fmt.Fprint(&sql, + EngineQuery(map[dbtypes.DBEngineType]string{ + dbtypes.DBEnginePgsql: "INSERT INTO voluntary_exits ", + dbtypes.DBEngineSqlite: "INSERT OR REPLACE INTO voluntary_exits ", + }), + "(slot_number, slot_index, slot_root, orphaned, validator)", + " VALUES ", + ) + argIdx := 0 + fieldCount := 5 + + args := make([]any, len(voluntaryExits)*fieldCount) + for i, voluntaryExit := range voluntaryExits { + if i > 0 { + fmt.Fprintf(&sql, ", ") + } + fmt.Fprintf(&sql, "(") + for f := 0; f < fieldCount; f++ { + if f > 0 { + fmt.Fprintf(&sql, ", ") + } + fmt.Fprintf(&sql, "$%v", argIdx+f+1) + + } + fmt.Fprintf(&sql, ")") + + args[argIdx+0] = voluntaryExit.SlotNumber + args[argIdx+1] = voluntaryExit.SlotIndex + args[argIdx+2] = voluntaryExit.SlotRoot + args[argIdx+3] = voluntaryExit.Orphaned + args[argIdx+4] = voluntaryExit.ValidatorIndex + argIdx += fieldCount + } + fmt.Fprint(&sql, EngineQuery(map[dbtypes.DBEngineType]string{ + dbtypes.DBEnginePgsql: " ON CONFLICT (slot_root, slot_index) DO UPDATE SET orphaned = excluded.orphaned", + dbtypes.DBEngineSqlite: "", + })) + + _, err := tx.Exec(sql.String(), args...) + if err != nil { + return err + } + return nil +} + +func GetVoluntaryExits(firstSlot uint64, limit uint32) []*dbtypes.VoluntaryExit { + var sql strings.Builder + args := []any{} + fmt.Fprint(&sql, ` + SELECT + slot_number, slot_index, slot_root, orphaned, validator + FROM voluntary_exits + `) + if firstSlot > 0 { + args = append(args, firstSlot) + fmt.Fprintf(&sql, " WHERE slot_number <= $%v ", len(args)) + } + + args = append(args, limit) + fmt.Fprintf(&sql, ` + ORDER BY slot_number DESC, slot_index DESC + LIMIT $%v + `, len(args)) + + voluntaryExits := []*dbtypes.VoluntaryExit{} + err := ReaderDb.Select(&voluntaryExits, sql.String(), args...) + if err != nil { + logger.Errorf("Error while fetching voluntary exits: %v", err) + return nil + } + return voluntaryExits +} + +func GetVoluntaryExitForValidator(validator uint64) *dbtypes.VoluntaryExit { + var sql strings.Builder + args := []any{ + validator, + } + fmt.Fprint(&sql, ` + SELECT + slot_number, slot_index, slot_root, orphaned, validator + FROM voluntary_exits + WHERE validator = $1 + `) + + voluntaryExit := &dbtypes.VoluntaryExit{} + err := ReaderDb.Get(&voluntaryExit, sql.String(), args...) + if err != nil { + return nil + } + return voluntaryExit +} + +func GetVoluntaryExitsFiltered(offset uint64, limit uint32, finalizedBlock uint64, filter *dbtypes.VoluntaryExitFilter) ([]*dbtypes.VoluntaryExit, uint64, error) { + var sql strings.Builder + args := []any{} + fmt.Fprint(&sql, ` + WITH cte AS ( + SELECT + slot_number, slot_index, slot_root, orphaned, validator + FROM voluntary_exits + `) + + if filter.ValidatorName != "" { + fmt.Fprint(&sql, ` + LEFT JOIN validator_names ON validator_names."index" = voluntary_exits.validator + `) + } + + 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 filter.MinIndex > 0 { + args = append(args, filter.MinIndex) + fmt.Fprintf(&sql, " %v validator >= $%v", filterOp, len(args)) + filterOp = "AND" + } + if filter.MaxIndex > 0 { + args = append(args, filter.MaxIndex) + fmt.Fprintf(&sql, " %v validator <= $%v", filterOp, 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 AND orphaned = true)", filterOp, len(args)) + filterOp = "AND" + } + if filter.ValidatorName != "" { + args = append(args, "%"+filter.ValidatorName+"%") + fmt.Fprintf(&sql, " %v ", filterOp) + fmt.Fprintf(&sql, EngineQuery(map[dbtypes.DBEngineType]string{ + dbtypes.DBEnginePgsql: ` validator_names.name ilike $%v `, + dbtypes.DBEngineSqlite: ` validator_names.name LIKE $%v `, + }), len(args)) + + filterOp = "AND" + } + + args = append(args, limit) + fmt.Fprintf(&sql, `) + SELECT + count(*) AS slot_number, + 0 AS slot_index, + null AS slot_root, + false AS orphaned, + 0 AS validator + 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") + + voluntaryExits := []*dbtypes.VoluntaryExit{} + err := ReaderDb.Select(&voluntaryExits, sql.String(), args...) + if err != nil { + logger.Errorf("Error while fetching filtered voluntary exits: %v", err) + return nil, 0, err + } + + return voluntaryExits[1:], voluntaryExits[0].SlotNumber, nil +} diff --git a/dbtypes/dbtypes.go b/dbtypes/dbtypes.go index 11ceaa2e..e5162e87 100644 --- a/dbtypes/dbtypes.go +++ b/dbtypes/dbtypes.go @@ -154,3 +154,29 @@ type Deposit struct { WithdrawalCredentials []byte `db:"withdrawalcredentials"` Amount uint64 `db:"amount"` } + +type VoluntaryExit struct { + SlotNumber uint64 `db:"slot_number"` + SlotIndex uint64 `db:"slot_index"` + SlotRoot []byte `db:"slot_root"` + Orphaned bool `db:"orphaned"` + ValidatorIndex uint64 `db:"validator"` +} + +type SlashingReason uint8 + +const ( + UnspecifiedSlashing SlashingReason = iota + ProposerSlashing + AttesterSlashing +) + +type Slashing struct { + SlotNumber uint64 `db:"slot_number"` + SlotIndex uint64 `db:"slot_index"` + SlotRoot []byte `db:"slot_root"` + Orphaned bool `db:"orphaned"` + ValidatorIndex uint64 `db:"validator"` + SlasherIndex uint64 `db:"slasher"` + Reason SlashingReason `db:"reason"` +} diff --git a/dbtypes/other.go b/dbtypes/other.go index 7e758981..ffa4d0f2 100644 --- a/dbtypes/other.go +++ b/dbtypes/other.go @@ -47,3 +47,23 @@ type DepositFilter struct { MaxAmount uint64 WithOrphaned uint8 } + +type VoluntaryExitFilter struct { + MinSlot uint64 + MaxSlot uint64 + MinIndex uint64 + MaxIndex uint64 + ValidatorName string + WithOrphaned uint8 +} + +type SlashingFilter struct { + MinSlot uint64 + MaxSlot uint64 + MinIndex uint64 + MaxIndex uint64 + ValidatorName string + SlasherName string + WithOrphaned uint8 + WithReason SlashingReason +} diff --git a/handlers/pageData.go b/handlers/pageData.go index 67c45516..b7724361 100644 --- a/handlers/pageData.go +++ b/handlers/pageData.go @@ -159,6 +159,20 @@ func createMenuItems(active string, isMain bool) []types.MainMenuItem { }, }, }, + { + Links: []types.NavigationLink{ + { + Label: "Voluntary Exits", + Path: "/validators/voluntary_exits", + Icon: "fa-user-slash", + }, + { + Label: "Slashings", + Path: "/validators/slashings", + Icon: "fa-user-slash", + }, + }, + }, }, }, } diff --git a/handlers/slashings.go b/handlers/slashings.go new file mode 100644 index 00000000..ce6d054b --- /dev/null +++ b/handlers/slashings.go @@ -0,0 +1,260 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "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" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/templates" + "github.com/ethpandaops/dora/types/models" + "github.com/ethpandaops/dora/utils" + "github.com/sirupsen/logrus" +) + +// Slashings will return the filtered "slashings" page using a go template +func Slashings(w http.ResponseWriter, r *http.Request) { + var templateFiles = append(layoutTemplateFiles, + "slashings/slashings.html", + "_svg/professor.html", + ) + + var pageTemplate = templates.GetTemplate(templateFiles...) + data := InitPageData(w, r, "validators", "/validators/slashings", "Slashings", 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 minIndex uint64 + var maxIndex uint64 + var vname string + var sname string + var withReason uint64 + 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.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.sname") { + sname = urlArgs.Get("f.sname") + } + if urlArgs.Has("f.reason") { + withReason, _ = strconv.ParseUint(urlArgs.Get("f.reason"), 10, 64) + } + 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 = getFilteredSlashingsPageData(pageIdx, pageSize, minSlot, maxSlot, minIndex, maxIndex, vname, sname, uint8(withReason), uint8(withOrphaned)) + } + if pageError != nil { + handlePageError(w, r, pageError) + return + } + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "slashings.go", "Slashings", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } +} + +func getFilteredSlashingsPageData(pageIdx uint64, pageSize uint64, minSlot uint64, maxSlot uint64, minIndex uint64, maxIndex uint64, vname string, sname string, withReason uint8, withOrphaned uint8) (*models.SlashingsPageData, error) { + pageData := &models.SlashingsPageData{} + pageCacheKey := fmt.Sprintf("slashings:%v:%v:%v:%v:%v:%v:%v:%v:%v:%v", pageIdx, pageSize, minSlot, maxSlot, minIndex, maxIndex, vname, sname, withReason, withOrphaned) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(_ *services.FrontendCacheProcessingPage) interface{} { + return buildFilteredSlashingsPageData(pageIdx, pageSize, minSlot, maxSlot, minIndex, maxIndex, vname, sname, withReason, withOrphaned) + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.SlashingsPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildFilteredSlashingsPageData(pageIdx uint64, pageSize uint64, minSlot uint64, maxSlot uint64, minIndex uint64, maxIndex uint64, vname string, sname string, withReason uint8, withOrphaned uint8) *models.SlashingsPageData { + 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 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 sname != "" { + filterArgs.Add("f.sname", sname) + } + if withReason != 0 { + filterArgs.Add("f.reason", fmt.Sprintf("%v", withReason)) + } + if withOrphaned != 0 { + filterArgs.Add("f.orphaned", fmt.Sprintf("%v", withOrphaned)) + } + + pageData := &models.SlashingsPageData{ + FilterMinSlot: minSlot, + FilterMaxSlot: maxSlot, + FilterMinIndex: minIndex, + FilterMaxIndex: maxIndex, + FilterValidatorName: vname, + FilterSlasherName: sname, + FilterWithReason: withReason, + FilterWithOrphaned: withOrphaned, + } + logrus.Debugf("slashings page called: %v:%v [%v,%v,%v,%v,%v,%v]", pageIdx, pageSize, minSlot, maxSlot, minIndex, maxIndex, vname, sname) + 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 slashings + finalizedEpoch, _ := services.GlobalBeaconService.GetFinalizedEpoch() + if finalizedEpoch < 0 { + finalizedEpoch = 0 + } + + slashingFilter := &dbtypes.SlashingFilter{ + MinSlot: minSlot, + MaxSlot: maxSlot, + MinIndex: minIndex, + MaxIndex: maxIndex, + ValidatorName: vname, + SlasherName: sname, + WithReason: dbtypes.SlashingReason(withReason), + WithOrphaned: withOrphaned, + } + + offset := (pageIdx - 1) * pageSize + + dbSlashings, totalRows, err := db.GetSlashingsFiltered(offset, uint32(pageSize), uint64(finalizedEpoch), slashingFilter) + if err != nil { + panic(err) + } + + validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet() + validatorActivityMap, validatorActivityMax := services.GlobalBeaconService.GetValidatorActivity() + + for _, slashing := range dbSlashings { + slashingData := &models.SlashingsPageDataSlashing{ + SlotNumber: slashing.SlotNumber, + SlotRoot: slashing.SlotRoot, + Time: utils.SlotToTime(slashing.SlotNumber), + Orphaned: slashing.Orphaned, + Reason: uint8(slashing.Reason), + ValidatorIndex: slashing.ValidatorIndex, + ValidatorName: services.GlobalBeaconService.GetValidatorName(slashing.ValidatorIndex), + SlasherIndex: slashing.SlasherIndex, + SlasherName: services.GlobalBeaconService.GetValidatorName(slashing.SlasherIndex), + ValidatorStatus: "", + } + + validator := validatorSetRsp[phase0.ValidatorIndex(slashing.ValidatorIndex)] + if validator == nil { + slashingData.ValidatorStatus = "Unknown" + } else { + slashingData.Balance = uint64(validator.Balance) + + if strings.HasPrefix(validator.Status.String(), "pending") { + slashingData.ValidatorStatus = "Pending" + } else if validator.Status == v1.ValidatorStateActiveOngoing { + slashingData.ValidatorStatus = "Active" + slashingData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveExiting { + slashingData.ValidatorStatus = "Exiting" + slashingData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveSlashed { + slashingData.ValidatorStatus = "Slashed" + slashingData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateExitedUnslashed { + slashingData.ValidatorStatus = "Exited" + } else if validator.Status == v1.ValidatorStateExitedSlashed { + slashingData.ValidatorStatus = "Slashed" + } else { + slashingData.ValidatorStatus = validator.Status.String() + } + + if slashingData.ShowUpcheck { + slashingData.UpcheckActivity = validatorActivityMap[uint64(validator.Index)] + slashingData.UpcheckMaximum = uint8(validatorActivityMax) + } + } + + pageData.Slashings = append(pageData.Slashings, slashingData) + } + pageData.SlashingCount = uint64(len(pageData.Slashings)) + + if pageData.SlashingCount > 0 { + pageData.FirstIndex = pageData.Slashings[0].SlotNumber + pageData.LastIndex = pageData.Slashings[pageData.SlashingCount-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/slashings?f&%v&c=%v", filterArgs.Encode(), pageData.PageSize) + pageData.PrevPageLink = fmt.Sprintf("/validators/slashings?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.PrevPageIndex) + pageData.NextPageLink = fmt.Sprintf("/validators/slashings?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.NextPageIndex) + pageData.LastPageLink = fmt.Sprintf("/validators/slashings?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.LastPageIndex) + + return pageData +} diff --git a/handlers/voluntary_exits.go b/handlers/voluntary_exits.go new file mode 100644 index 00000000..9eed9dae --- /dev/null +++ b/handlers/voluntary_exits.go @@ -0,0 +1,240 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "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" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/templates" + "github.com/ethpandaops/dora/types/models" + "github.com/ethpandaops/dora/utils" + "github.com/sirupsen/logrus" +) + +// VoluntaryExits will return the filtered "voluntary_exits" page using a go template +func VoluntaryExits(w http.ResponseWriter, r *http.Request) { + var templateFiles = append(layoutTemplateFiles, + "voluntary_exits/voluntary_exits.html", + "_svg/professor.html", + ) + + var pageTemplate = templates.GetTemplate(templateFiles...) + data := InitPageData(w, r, "validators", "/validators/voluntary_exits", "Voluntary Exits", 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 minIndex uint64 + var maxIndex uint64 + var vname 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.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) + } + } else { + withOrphaned = 1 + } + var pageError error + pageError = services.GlobalCallRateLimiter.CheckCallLimit(r, 2) + if pageError == nil { + data.Data, pageError = getFilteredVoluntaryExitsPageData(pageIdx, pageSize, minSlot, maxSlot, minIndex, maxIndex, vname, uint8(withOrphaned)) + } + if pageError != nil { + handlePageError(w, r, pageError) + return + } + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "voluntary_exits.go", "VoluntaryExits", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } +} + +func getFilteredVoluntaryExitsPageData(pageIdx uint64, pageSize uint64, minSlot uint64, maxSlot uint64, minIndex uint64, maxIndex uint64, vname string, withOrphaned uint8) (*models.VoluntaryExitsPageData, error) { + pageData := &models.VoluntaryExitsPageData{} + pageCacheKey := fmt.Sprintf("voluntary_exits:%v:%v:%v:%v:%v:%v:%v:%v", pageIdx, pageSize, minSlot, maxSlot, minIndex, maxIndex, vname, withOrphaned) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(_ *services.FrontendCacheProcessingPage) interface{} { + return buildFilteredVoluntaryExitsPageData(pageIdx, pageSize, minSlot, maxSlot, minIndex, maxIndex, vname, withOrphaned) + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.VoluntaryExitsPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildFilteredVoluntaryExitsPageData(pageIdx uint64, pageSize uint64, minSlot uint64, maxSlot uint64, minIndex uint64, maxIndex uint64, vname string, withOrphaned uint8) *models.VoluntaryExitsPageData { + 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 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)) + } + + pageData := &models.VoluntaryExitsPageData{ + FilterMinSlot: minSlot, + FilterMaxSlot: maxSlot, + FilterMinIndex: minIndex, + FilterMaxIndex: maxIndex, + FilterValidatorName: vname, + FilterWithOrphaned: withOrphaned, + } + logrus.Debugf("voluntary_exits 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 + finalizedEpoch, _ := services.GlobalBeaconService.GetFinalizedEpoch() + if finalizedEpoch < 0 { + finalizedEpoch = 0 + } + + voluntaryExitFilter := &dbtypes.VoluntaryExitFilter{ + MinSlot: minSlot, + MaxSlot: maxSlot, + MinIndex: minIndex, + MaxIndex: maxIndex, + ValidatorName: vname, + WithOrphaned: withOrphaned, + } + + offset := (pageIdx - 1) * pageSize + + dbVoluntaryExits, totalRows, err := db.GetVoluntaryExitsFiltered(offset, uint32(pageSize), uint64(finalizedEpoch), voluntaryExitFilter) + if err != nil { + panic(err) + } + + validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet() + validatorActivityMap, validatorActivityMax := services.GlobalBeaconService.GetValidatorActivity() + + for _, voluntaryExit := range dbVoluntaryExits { + voluntaryExitData := &models.VoluntaryExitsPageDataExit{ + SlotNumber: voluntaryExit.SlotNumber, + SlotRoot: voluntaryExit.SlotRoot, + Time: utils.SlotToTime(voluntaryExit.SlotNumber), + Orphaned: voluntaryExit.Orphaned, + ValidatorIndex: voluntaryExit.ValidatorIndex, + ValidatorName: services.GlobalBeaconService.GetValidatorName(voluntaryExit.ValidatorIndex), + ValidatorStatus: "", + } + + validator := validatorSetRsp[phase0.ValidatorIndex(voluntaryExit.ValidatorIndex)] + if validator == nil { + voluntaryExitData.ValidatorStatus = "Unknown" + } else { + voluntaryExitData.PublicKey = validator.Validator.PublicKey[:] + voluntaryExitData.WithdrawalCreds = validator.Validator.WithdrawalCredentials + + if strings.HasPrefix(validator.Status.String(), "pending") { + voluntaryExitData.ValidatorStatus = "Pending" + } else if validator.Status == v1.ValidatorStateActiveOngoing { + voluntaryExitData.ValidatorStatus = "Active" + voluntaryExitData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveExiting { + voluntaryExitData.ValidatorStatus = "Exiting" + voluntaryExitData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateActiveSlashed { + voluntaryExitData.ValidatorStatus = "Slashed" + voluntaryExitData.ShowUpcheck = true + } else if validator.Status == v1.ValidatorStateExitedUnslashed { + voluntaryExitData.ValidatorStatus = "Exited" + } else if validator.Status == v1.ValidatorStateExitedSlashed { + voluntaryExitData.ValidatorStatus = "Slashed" + } else { + voluntaryExitData.ValidatorStatus = validator.Status.String() + } + + if voluntaryExitData.ShowUpcheck { + voluntaryExitData.UpcheckActivity = validatorActivityMap[uint64(validator.Index)] + voluntaryExitData.UpcheckMaximum = uint8(validatorActivityMax) + } + } + + pageData.VoluntaryExits = append(pageData.VoluntaryExits, voluntaryExitData) + } + pageData.ExitCount = uint64(len(pageData.VoluntaryExits)) + + if pageData.ExitCount > 0 { + pageData.FirstIndex = pageData.VoluntaryExits[0].SlotNumber + pageData.LastIndex = pageData.VoluntaryExits[pageData.ExitCount-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/voluntary_exits?f&%v&c=%v", filterArgs.Encode(), pageData.PageSize) + pageData.PrevPageLink = fmt.Sprintf("/validators/voluntary_exits?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.PrevPageIndex) + pageData.NextPageLink = fmt.Sprintf("/validators/voluntary_exits?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.NextPageIndex) + pageData.LastPageLink = fmt.Sprintf("/validators/voluntary_exits?f&%v&c=%v&p=%v", filterArgs.Encode(), pageData.PageSize, pageData.LastPageIndex) + + return pageData +} diff --git a/indexer/cache_logic.go b/indexer/cache_logic.go index 598930ae..cde28e14 100644 --- a/indexer/cache_logic.go +++ b/indexer/cache_logic.go @@ -411,6 +411,18 @@ func (cache *indexerCache) processCachePersistence() error { return err } + err = persistBlockVoluntaryExits(block, true, tx) + if err != nil { + logger.Errorf("error persisting unfinalized voluntary exits: %v", err) + return err + } + + err = persistBlockSlashings(block, true, tx) + if err != nil { + logger.Errorf("error persisting unfinalized slashings: %v", err) + return err + } + block.isInUnfinalizedDb = true } } diff --git a/indexer/write_db.go b/indexer/write_db.go index 682c19e7..799fc693 100644 --- a/indexer/write_db.go +++ b/indexer/write_db.go @@ -7,6 +7,7 @@ import ( "github.com/ethpandaops/dora/dbtypes" "github.com/ethpandaops/dora/utils" "github.com/jmoiron/sqlx" + "github.com/juliangruber/go-intersect" ) func persistSlotAssignments(epochStats *EpochStats, tx *sqlx.Tx) error { @@ -73,6 +74,18 @@ func persistBlockData(block *CacheBlock, epochStats *EpochStats, depositIndex *u return err } + // insert voluntary exits + err = persistBlockVoluntaryExits(block, orphaned, tx) + if err != nil { + return err + } + + // insert slashings + err = persistBlockSlashings(block, orphaned, tx) + if err != nil { + return err + } + return nil } @@ -340,3 +353,129 @@ func buildDbDeposits(block *CacheBlock, depositIndex *uint64) []*dbtypes.Deposit return dbDeposits } + +func persistBlockVoluntaryExits(block *CacheBlock, orphaned bool, tx *sqlx.Tx) error { + // insert voluntary exits + dbVoluntaryExits := buildDbVoluntaryExits(block) + if orphaned { + for idx := range dbVoluntaryExits { + dbVoluntaryExits[idx].Orphaned = true + } + } + + if len(dbVoluntaryExits) > 0 { + err := db.InsertVoluntaryExits(dbVoluntaryExits, tx) + if err != nil { + return fmt.Errorf("error inserting voluntary exits: %v", err) + } + } + + return nil +} + +func buildDbVoluntaryExits(block *CacheBlock) []*dbtypes.VoluntaryExit { + blockBody := block.GetBlockBody() + if blockBody == nil { + return nil + } + + voluntaryExits, err := blockBody.VoluntaryExits() + if err != nil { + return nil + } + + dbVoluntaryExits := make([]*dbtypes.VoluntaryExit, len(voluntaryExits)) + for idx, voluntaryExit := range voluntaryExits { + dbVoluntaryExit := &dbtypes.VoluntaryExit{ + SlotNumber: block.Slot, + SlotIndex: uint64(idx), + SlotRoot: block.Root, + Orphaned: false, + ValidatorIndex: uint64(voluntaryExit.Message.ValidatorIndex), + } + + dbVoluntaryExits[idx] = dbVoluntaryExit + } + + return dbVoluntaryExits +} + +func persistBlockSlashings(block *CacheBlock, orphaned bool, tx *sqlx.Tx) error { + // insert slashings + dbSlashings := buildDbSlashings(block) + if orphaned { + for idx := range dbSlashings { + dbSlashings[idx].Orphaned = true + } + } + + if len(dbSlashings) > 0 { + err := db.InsertSlashings(dbSlashings, tx) + if err != nil { + return fmt.Errorf("error inserting slashings: %v", err) + } + } + + return nil +} + +func buildDbSlashings(block *CacheBlock) []*dbtypes.Slashing { + blockBody := block.GetBlockBody() + if blockBody == nil { + return nil + } + + proposerSlashings, err := blockBody.ProposerSlashings() + if err != nil { + return nil + } + + attesterSlashings, err := blockBody.AttesterSlashings() + if err != nil { + return nil + } + + proposerIndex, err := blockBody.ProposerIndex() + if err != nil { + return nil + } + + dbSlashings := []*dbtypes.Slashing{} + slashingIndex := 0 + + for _, proposerSlashing := range proposerSlashings { + dbSlashing := &dbtypes.Slashing{ + SlotNumber: block.Slot, + SlotIndex: uint64(slashingIndex), + SlotRoot: block.Root, + Orphaned: false, + ValidatorIndex: uint64(proposerSlashing.SignedHeader1.Message.ProposerIndex), + SlasherIndex: uint64(proposerIndex), + Reason: dbtypes.ProposerSlashing, + } + slashingIndex++ + dbSlashings = append(dbSlashings, dbSlashing) + } + + for _, attesterSlashing := range attesterSlashings { + inter := intersect.Simple(attesterSlashing.Attestation1.AttestingIndices, attesterSlashing.Attestation2.AttestingIndices) + for _, j := range inter { + valIdx := j.(uint64) + + dbSlashing := &dbtypes.Slashing{ + SlotNumber: block.Slot, + SlotIndex: uint64(slashingIndex), + SlotRoot: block.Root, + Orphaned: false, + ValidatorIndex: uint64(valIdx), + SlasherIndex: uint64(proposerIndex), + Reason: dbtypes.AttesterSlashing, + } + dbSlashings = append(dbSlashings, dbSlashing) + } + + slashingIndex++ + } + + return dbSlashings +} diff --git a/templates/slashings/slashings.html b/templates/slashings/slashings.html new file mode 100644 index 00000000..6bf1fb2c --- /dev/null +++ b/templates/slashings/slashings.html @@ -0,0 +1,250 @@ +{{ define "page" }} +
+
+

+ Slashings +

+ +
+ +
+
+ +
+
+ Slashings Filters +
+
+
+
+
+
+
+ Slot Number +
+
+
+ +
+
+ - +
+
+ +
+
+
+
+
+ Validator Index +
+
+
+ +
+
+ - +
+
+ +
+
+
+
+
+ Validator Name +
+
+ +
+
+
+
+ Slasher Name +
+
+ +
+
+
+
+
+
+
+
+ Slashing Reason +
+
+ +
+
+
+
+ Orphaned Exits +
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + {{ if gt .SlashingCount 0 }} + + {{ range $i, $slashing := .Slashings }} + + {{ if $slashing.Orphaned }} + + {{ else }} + + {{ end }} + + + + + + + + {{ end }} + + {{ else }} + + + + + + + + {{ end }} +
SlotTimeValidatorReasonValidator StateValidator BalanceSlasher
{{ formatAddCommas $slashing.SlotNumber }}{{ formatAddCommas $slashing.SlotNumber }}{{ formatRecentTimeShort $slashing.Time }}{{ formatValidator $slashing.ValidatorIndex $slashing.ValidatorName }} + {{ if eq $slashing.Reason 1 }} + Proposer Slashing + {{ else if eq $slashing.Reason 2 }} + Attester Slashing + {{ else }} + Included + {{ end }} + + {{- $slashing.ValidatorStatus -}} + {{- if $slashing.ShowUpcheck -}} + {{- if eq $slashing.UpcheckActivity $slashing.UpcheckMaximum }} + + {{- else if gt $slashing.UpcheckActivity 0 }} + + {{- else }} + + {{- end -}} + {{- end -}} + {{ formatFullEthFromGwei $slashing.Balance }}{{ formatValidator $slashing.SlasherIndex $slashing.SlasherName }}
+
+ {{ template "professor_svg" }} +
+
+
+ {{ if gt .TotalPages 1 }} +
+
+
+
Showing slashings from slot {{ .FirstIndex }} to {{ .LastIndex }}
+
+
+
+
+ +
+
+
+ {{ end }} +
+ +
+
+{{ end }} +{{ define "js" }} +{{ end }} +{{ define "css" }} + +{{ end }} \ No newline at end of file diff --git a/templates/voluntary_exits/voluntary_exits.html b/templates/voluntary_exits/voluntary_exits.html new file mode 100644 index 00000000..23e5bac4 --- /dev/null +++ b/templates/voluntary_exits/voluntary_exits.html @@ -0,0 +1,242 @@ +{{ define "page" }} +
+
+

+ Voluntary Exits +

+ +
+ +
+
+ +
+
+ Voluntary Exits Filters +
+
+
+
+
+
+
+ Slot Number +
+
+
+ +
+
+ - +
+
+ +
+
+
+
+
+ Validator Index +
+
+
+ +
+
+ - +
+
+ +
+
+
+
+
+ Validator Name +
+
+ +
+
+
+
+
+
+
+
+ Orphaned Exits +
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + {{ if gt .ExitCount 0 }} + + {{ range $i, $voluntaryExit := .VoluntaryExits }} + + {{ if $voluntaryExit.Orphaned }} + + {{ else }} + + {{ end }} + + + + + + + + {{ end }} + + {{ else }} + + + + + + + + {{ end }} +
SlotTimeValidatorPublic KeyWithdrawal CredIncl. StatusValidator State
{{ formatAddCommas $voluntaryExit.SlotNumber }}{{ formatAddCommas $voluntaryExit.SlotNumber }}{{ formatRecentTimeShort $voluntaryExit.Time }}{{ formatValidator $voluntaryExit.ValidatorIndex $voluntaryExit.ValidatorName }} +
+ + 0x{{ printf "%x" $voluntaryExit.PublicKey }} + +
+ +
+
+
+ + {{ formatWithdawalCredentials $voluntaryExit.WithdrawalCreds }} + + + + {{ if $voluntaryExit.Orphaned }} + Orphaned + {{ else }} + Included + {{ end }} + + {{- $voluntaryExit.ValidatorStatus -}} + {{- if $voluntaryExit.ShowUpcheck -}} + {{- if eq $voluntaryExit.UpcheckActivity $voluntaryExit.UpcheckMaximum }} + + {{- else if gt $voluntaryExit.UpcheckActivity 0 }} + + {{- else }} + + {{- end -}} + {{- end -}} +
+
+ {{ template "professor_svg" }} +
+
+
+ {{ if gt .TotalPages 1 }} +
+
+
+
Showing voluntary exits from slot {{ .FirstIndex }} to {{ .LastIndex }}
+
+
+
+
+ +
+
+
+ {{ end }} +
+ +
+
+{{ end }} +{{ define "js" }} +{{ end }} +{{ define "css" }} + +{{ end }} \ No newline at end of file diff --git a/types/models/slashings.go b/types/models/slashings.go new file mode 100644 index 00000000..5145c44e --- /dev/null +++ b/types/models/slashings.go @@ -0,0 +1,52 @@ +package models + +import ( + "time" +) + +// SlashingsPageData is a struct to hold info for the slashings page +type SlashingsPageData struct { + FilterMinSlot uint64 `json:"filter_mins"` + FilterMaxSlot uint64 `json:"filter_maxs"` + FilterMinIndex uint64 `json:"filter_mini"` + FilterMaxIndex uint64 `json:"filter_maxi"` + FilterValidatorName string `json:"filter_vname"` + FilterSlasherName string `json:"filter_sname"` + FilterWithReason uint8 `json:"filter_reason"` + FilterWithOrphaned uint8 `json:"filter_orphaned"` + + Slashings []*SlashingsPageDataSlashing `json:"slashings"` + SlashingCount uint64 `json:"slashing_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 SlashingsPageDataSlashing struct { + SlotNumber uint64 `json:"slot"` + SlotRoot []byte `json:"slot_root"` + Time time.Time `json:"time"` + Orphaned bool `json:"orphaned"` + ValidatorIndex uint64 `json:"vindex"` + ValidatorName string `json:"vname"` + Reason uint8 `json:"reason"` + ValidatorStatus string `json:"vstatus"` + ShowUpcheck bool `json:"show_upcheck"` + UpcheckActivity uint8 `json:"upcheck_act"` + UpcheckMaximum uint8 `json:"upcheck_max"` + Balance uint64 `json:"balance"` + SlasherIndex uint64 `json:"sindex"` + SlasherName string `json:"sname"` +} diff --git a/types/models/voluntary_exits.go b/types/models/voluntary_exits.go new file mode 100644 index 00000000..af314e0a --- /dev/null +++ b/types/models/voluntary_exits.go @@ -0,0 +1,48 @@ +package models + +import ( + "time" +) + +// VoluntaryExitsPageData is a struct to hold info for the voluntary_exits page +type VoluntaryExitsPageData struct { + FilterMinSlot uint64 `json:"filter_mins"` + FilterMaxSlot uint64 `json:"filter_maxs"` + FilterMinIndex uint64 `json:"filter_mini"` + FilterMaxIndex uint64 `json:"filter_maxi"` + FilterValidatorName string `json:"filter_vname"` + FilterWithOrphaned uint8 `json:"filter_orphaned"` + + VoluntaryExits []*VoluntaryExitsPageDataExit `json:"exits"` + ExitCount uint64 `json:"exit_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 VoluntaryExitsPageDataExit struct { + SlotNumber uint64 `json:"slot"` + SlotRoot []byte `json:"slot_root"` + Time time.Time `json:"time"` + Orphaned bool `json:"orphaned"` + ValidatorIndex uint64 `json:"vindex"` + ValidatorName string `json:"vname"` + PublicKey []byte `json:"pubkey"` + WithdrawalCreds []byte `json:"wdcreds"` + ValidatorStatus string `json:"vstatus"` + ShowUpcheck bool `json:"show_upcheck"` + UpcheckActivity uint8 `json:"upcheck_act"` + UpcheckMaximum uint8 `json:"upcheck_max"` +}