diff --git a/packages/api/src/beacon/routes/beacon/state.ts b/packages/api/src/beacon/routes/beacon/state.ts index e3cfe2e16edf..1489f48c297e 100644 --- a/packages/api/src/beacon/routes/beacon/state.ts +++ b/packages/api/src/beacon/routes/beacon/state.ts @@ -2,7 +2,7 @@ import {ContainerType, ValueOf} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import {MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params"; -import {phase0, CommitteeIndex, Slot, Epoch, ssz, RootHex, StringType} from "@lodestar/types"; +import {phase0, CommitteeIndex, Slot, Epoch, ssz, RootHex, StringType, ValidatorStatus} from "@lodestar/types"; import {Endpoint, RequestCodec, RouteDefinitions, Schema} from "../../../utils/index.js"; import {ArrayOf, JsonOnlyReq} from "../../../utils/codecs.js"; import {ExecutionOptimisticAndFinalizedCodec, ExecutionOptimisticAndFinalizedMeta} from "../../../utils/metadata.js"; @@ -24,17 +24,7 @@ export type StateArgs = { export type ValidatorId = string | number; -export type ValidatorStatus = - | "active" - | "pending_initialized" - | "pending_queued" - | "active_ongoing" - | "active_exiting" - | "active_slashed" - | "exited_unslashed" - | "exited_slashed" - | "withdrawal_possible" - | "withdrawal_done"; +export type {ValidatorStatus}; export const RandaoResponseType = new ContainerType({ randao: ssz.Root, diff --git a/packages/beacon-node/src/api/impl/beacon/state/index.ts b/packages/beacon-node/src/api/impl/beacon/state/index.ts index 669aaa9d1fb3..2bf758a8e286 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/index.ts @@ -9,16 +9,11 @@ import { getRandaoMix, } from "@lodestar/state-transition"; import {EPOCHS_PER_HISTORICAL_VECTOR} from "@lodestar/params"; +import {getValidatorStatus} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; import {ApiError} from "../../errors.js"; import {ApiModules} from "../../types.js"; -import { - filterStateValidatorsByStatus, - getStateValidatorIndex, - getValidatorStatus, - getStateResponse, - toValidatorResponse, -} from "./utils.js"; +import {filterStateValidatorsByStatus, getStateValidatorIndex, getStateResponse, toValidatorResponse} from "./utils.js"; export function getBeaconStateApi({ chain, diff --git a/packages/beacon-node/src/api/impl/beacon/state/utils.ts b/packages/beacon-node/src/api/impl/beacon/state/utils.ts index de21c6a244ed..5e9d3be01221 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/utils.ts @@ -1,8 +1,8 @@ import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {routes} from "@lodestar/api"; -import {FAR_FUTURE_EPOCH, GENESIS_SLOT} from "@lodestar/params"; +import {GENESIS_SLOT} from "@lodestar/params"; import {BeaconStateAllForks} from "@lodestar/state-transition"; -import {BLSPubkey, Epoch, phase0, RootHex, Slot, ValidatorIndex} from "@lodestar/types"; +import {BLSPubkey, Epoch, getValidatorStatus, phase0, RootHex, Slot, ValidatorIndex} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; import {CheckpointWithHex, IForkChoice} from "@lodestar/fork-choice"; import {IBeaconChain} from "../../../../chain/index.js"; @@ -83,38 +83,6 @@ export async function getStateResponseWithRegen( return res; } -/** - * Get the status of the validator - * based on conditions outlined in https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ - */ -export function getValidatorStatus(validator: phase0.Validator, currentEpoch: Epoch): routes.beacon.ValidatorStatus { - // pending - if (validator.activationEpoch > currentEpoch) { - if (validator.activationEligibilityEpoch === FAR_FUTURE_EPOCH) { - return "pending_initialized"; - } else if (validator.activationEligibilityEpoch < FAR_FUTURE_EPOCH) { - return "pending_queued"; - } - } - // active - if (validator.activationEpoch <= currentEpoch && currentEpoch < validator.exitEpoch) { - if (validator.exitEpoch === FAR_FUTURE_EPOCH) { - return "active_ongoing"; - } else if (validator.exitEpoch < FAR_FUTURE_EPOCH) { - return validator.slashed ? "active_slashed" : "active_exiting"; - } - } - // exited - if (validator.exitEpoch <= currentEpoch && currentEpoch < validator.withdrawableEpoch) { - return validator.slashed ? "exited_slashed" : "exited_unslashed"; - } - // withdrawal - if (validator.withdrawableEpoch <= currentEpoch) { - return validator.effectiveBalance !== 0 ? "withdrawal_possible" : "withdrawal_done"; - } - throw new Error("ValidatorStatus unknown"); -} - export function toValidatorResponse( index: ValidatorIndex, validator: phase0.Validator, diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 2347d7086d46..8a500125ad06 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -41,6 +41,7 @@ import { BeaconBlock, BlockContents, BlindedBeaconBlock, + getValidatorStatus, } from "@lodestar/types"; import {ExecutionStatus, DataAvailabilityStatus} from "@lodestar/fork-choice"; import {fromHex, toHex, resolveOrRacePromises, prettyWeiToEth, toRootHex} from "@lodestar/utils"; @@ -61,7 +62,6 @@ import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/va import {CommitteeSubscription} from "../../../network/subnets/index.js"; import {ApiModules} from "../types.js"; import {RegenCaller} from "../../../chain/regen/index.js"; -import {getValidatorStatus} from "../beacon/state/utils.js"; import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js"; import {SCHEDULER_LOOKAHEAD_FACTOR} from "../../../chain/prepareNextSlot.js"; import {ChainEvent, CheckpointHex, CommonBlockBody} from "../../../chain/index.js"; diff --git a/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts index 79a8f3383e5d..39c936c0d025 100644 --- a/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts +++ b/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts @@ -1,107 +1,9 @@ import {describe, it, expect} from "vitest"; import {toHexString} from "@chainsafe/ssz"; -import {phase0} from "@lodestar/types"; -import {getValidatorStatus, getStateValidatorIndex} from "../../../../../../src/api/impl/beacon/state/utils.js"; +import {getStateValidatorIndex} from "../../../../../../src/api/impl/beacon/state/utils.js"; import {generateCachedAltairState} from "../../../../../utils/state.js"; describe("beacon state api utils", function () { - describe("getValidatorStatus", function () { - it("should return PENDING_INITIALIZED", function () { - const validator = { - activationEpoch: 1, - activationEligibilityEpoch: Infinity, - } as phase0.Validator; - const currentEpoch = 0; - const status = getValidatorStatus(validator, currentEpoch); - expect(status).toBe("pending_initialized"); - }); - it("should return PENDING_QUEUED", function () { - const validator = { - activationEpoch: 1, - activationEligibilityEpoch: 101010101101010, - } as phase0.Validator; - const currentEpoch = 0; - const status = getValidatorStatus(validator, currentEpoch); - expect(status).toBe("pending_queued"); - }); - it("should return ACTIVE_ONGOING", function () { - const validator = { - activationEpoch: 1, - exitEpoch: Infinity, - } as phase0.Validator; - const currentEpoch = 1; - const status = getValidatorStatus(validator, currentEpoch); - expect(status).toBe("active_ongoing"); - }); - it("should return ACTIVE_SLASHED", function () { - const validator = { - activationEpoch: 1, - exitEpoch: 101010101101010, - slashed: true, - } as phase0.Validator; - const currentEpoch = 1; - const status = getValidatorStatus(validator, currentEpoch); - expect(status).toBe("active_slashed"); - }); - it("should return ACTIVE_EXITING", function () { - const validator = { - activationEpoch: 1, - exitEpoch: 101010101101010, - slashed: false, - } as phase0.Validator; - const currentEpoch = 1; - const status = getValidatorStatus(validator, currentEpoch); - expect(status).toBe("active_exiting"); - }); - it("should return EXITED_SLASHED", function () { - const validator = { - exitEpoch: 1, - withdrawableEpoch: 3, - slashed: true, - } as phase0.Validator; - const currentEpoch = 2; - const status = getValidatorStatus(validator, currentEpoch); - expect(status).toBe("exited_slashed"); - }); - it("should return EXITED_UNSLASHED", function () { - const validator = { - exitEpoch: 1, - withdrawableEpoch: 3, - slashed: false, - } as phase0.Validator; - const currentEpoch = 2; - const status = getValidatorStatus(validator, currentEpoch); - expect(status).toBe("exited_unslashed"); - }); - it("should return WITHDRAWAL_POSSIBLE", function () { - const validator = { - withdrawableEpoch: 1, - effectiveBalance: 32, - } as phase0.Validator; - const currentEpoch = 1; - const status = getValidatorStatus(validator, currentEpoch); - expect(status).toBe("withdrawal_possible"); - }); - it("should return WITHDRAWAL_DONE", function () { - const validator = { - withdrawableEpoch: 1, - effectiveBalance: 0, - } as phase0.Validator; - const currentEpoch = 1; - const status = getValidatorStatus(validator, currentEpoch); - expect(status).toBe("withdrawal_done"); - }); - it("should error", function () { - const validator = {} as phase0.Validator; - const currentEpoch = 0; - try { - getValidatorStatus(validator, currentEpoch); - } catch (error) { - expect(error).toHaveProperty("message", "ValidatorStatus unknown"); - } - }); - }); - describe("getStateValidatorIndex", () => { const state = generateCachedAltairState(); const pubkey2index = state.epochCtx.pubkey2index; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index e0745834c7d1..dc9139dc967e 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -9,3 +9,4 @@ export * from "./utils/typeguards.js"; export {StringType, stringType} from "./utils/stringType.js"; // Container utils export * from "./utils/container.js"; +export * from "./utils/validatorStatus.js"; diff --git a/packages/types/src/utils/validatorStatus.ts b/packages/types/src/utils/validatorStatus.ts new file mode 100644 index 000000000000..e14a4b14c412 --- /dev/null +++ b/packages/types/src/utils/validatorStatus.ts @@ -0,0 +1,46 @@ +import {FAR_FUTURE_EPOCH} from "@lodestar/params"; +import {Epoch, phase0} from "../types.js"; + +export type ValidatorStatus = + | "active" + | "pending_initialized" + | "pending_queued" + | "active_ongoing" + | "active_exiting" + | "active_slashed" + | "exited_unslashed" + | "exited_slashed" + | "withdrawal_possible" + | "withdrawal_done"; + +/** + * Get the status of the validator + * based on conditions outlined in https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ + */ +export function getValidatorStatus(validator: phase0.Validator, currentEpoch: Epoch): ValidatorStatus { + // pending + if (validator.activationEpoch > currentEpoch) { + if (validator.activationEligibilityEpoch === FAR_FUTURE_EPOCH) { + return "pending_initialized"; + } else if (validator.activationEligibilityEpoch < FAR_FUTURE_EPOCH) { + return "pending_queued"; + } + } + // active + if (validator.activationEpoch <= currentEpoch && currentEpoch < validator.exitEpoch) { + if (validator.exitEpoch === FAR_FUTURE_EPOCH) { + return "active_ongoing"; + } else if (validator.exitEpoch < FAR_FUTURE_EPOCH) { + return validator.slashed ? "active_slashed" : "active_exiting"; + } + } + // exited + if (validator.exitEpoch <= currentEpoch && currentEpoch < validator.withdrawableEpoch) { + return validator.slashed ? "exited_slashed" : "exited_unslashed"; + } + // withdrawal + if (validator.withdrawableEpoch <= currentEpoch) { + return validator.effectiveBalance !== 0 ? "withdrawal_possible" : "withdrawal_done"; + } + throw new Error("ValidatorStatus unknown"); +} diff --git a/packages/types/test/unit/validatorStatus.test.ts b/packages/types/test/unit/validatorStatus.test.ts new file mode 100644 index 000000000000..8d04c0f98e3d --- /dev/null +++ b/packages/types/test/unit/validatorStatus.test.ts @@ -0,0 +1,100 @@ +import {describe, it, expect} from "vitest"; +import {getValidatorStatus} from "../../src/utils/validatorStatus.js"; +import {phase0} from "../../src/types.js"; + +describe("getValidatorStatus", function () { + it("should return PENDING_INITIALIZED", function () { + const validator = { + activationEpoch: 1, + activationEligibilityEpoch: Infinity, + } as phase0.Validator; + const currentEpoch = 0; + const status = getValidatorStatus(validator, currentEpoch); + expect(status).toBe("pending_initialized"); + }); + it("should return PENDING_QUEUED", function () { + const validator = { + activationEpoch: 1, + activationEligibilityEpoch: 101010101101010, + } as phase0.Validator; + const currentEpoch = 0; + const status = getValidatorStatus(validator, currentEpoch); + expect(status).toBe("pending_queued"); + }); + it("should return ACTIVE_ONGOING", function () { + const validator = { + activationEpoch: 1, + exitEpoch: Infinity, + } as phase0.Validator; + const currentEpoch = 1; + const status = getValidatorStatus(validator, currentEpoch); + expect(status).toBe("active_ongoing"); + }); + it("should return ACTIVE_SLASHED", function () { + const validator = { + activationEpoch: 1, + exitEpoch: 101010101101010, + slashed: true, + } as phase0.Validator; + const currentEpoch = 1; + const status = getValidatorStatus(validator, currentEpoch); + expect(status).toBe("active_slashed"); + }); + it("should return ACTIVE_EXITING", function () { + const validator = { + activationEpoch: 1, + exitEpoch: 101010101101010, + slashed: false, + } as phase0.Validator; + const currentEpoch = 1; + const status = getValidatorStatus(validator, currentEpoch); + expect(status).toBe("active_exiting"); + }); + it("should return EXITED_SLASHED", function () { + const validator = { + exitEpoch: 1, + withdrawableEpoch: 3, + slashed: true, + } as phase0.Validator; + const currentEpoch = 2; + const status = getValidatorStatus(validator, currentEpoch); + expect(status).toBe("exited_slashed"); + }); + it("should return EXITED_UNSLASHED", function () { + const validator = { + exitEpoch: 1, + withdrawableEpoch: 3, + slashed: false, + } as phase0.Validator; + const currentEpoch = 2; + const status = getValidatorStatus(validator, currentEpoch); + expect(status).toBe("exited_unslashed"); + }); + it("should return WITHDRAWAL_POSSIBLE", function () { + const validator = { + withdrawableEpoch: 1, + effectiveBalance: 32, + } as phase0.Validator; + const currentEpoch = 1; + const status = getValidatorStatus(validator, currentEpoch); + expect(status).toBe("withdrawal_possible"); + }); + it("should return WITHDRAWAL_DONE", function () { + const validator = { + withdrawableEpoch: 1, + effectiveBalance: 0, + } as phase0.Validator; + const currentEpoch = 1; + const status = getValidatorStatus(validator, currentEpoch); + expect(status).toBe("withdrawal_done"); + }); + it("should error", function () { + const validator = {} as phase0.Validator; + const currentEpoch = 0; + try { + getValidatorStatus(validator, currentEpoch); + } catch (error) { + expect(error).toHaveProperty("message", "ValidatorStatus unknown"); + } + }); +});