From 669239be80b8dea985890359344ae3110e41e913 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 11 Apr 2024 18:41:38 +0100 Subject: [PATCH] feat: add POST endpoints for validators and validator_balances (#6655) * feat: add POST endpoints for validators and validator_balances * Update getStateValidatorIndex --- .../api/src/beacon/routes/beacon/state.ts | 90 +++++++++++++++++++ .../api/test/unit/beacon/oapiSpec.test.ts | 3 - .../api/test/unit/beacon/testData/beacon.ts | 8 ++ .../src/api/impl/beacon/state/index.ts | 8 ++ .../src/api/impl/beacon/state/utils.ts | 31 ++++--- .../unit/api/impl/beacon/state/utils.test.ts | 16 ++-- .../cli/test/utils/mockBeaconApiServer.ts | 3 + 7 files changed, 138 insertions(+), 21 deletions(-) diff --git a/packages/api/src/beacon/routes/beacon/state.ts b/packages/api/src/beacon/routes/beacon/state.ts index e9897eb3e07c..0c3875f8a3de 100644 --- a/packages/api/src/beacon/routes/beacon/state.ts +++ b/packages/api/src/beacon/routes/beacon/state.ts @@ -2,6 +2,7 @@ import {ContainerType} from "@chainsafe/ssz"; import {phase0, CommitteeIndex, Slot, ValidatorIndex, Epoch, Root, ssz, StringType, RootHex} from "@lodestar/types"; import {ApiClientResponse} from "../../../interfaces.js"; import {HttpStatusCode} from "../../../utils/client/httpStatusCode.js"; +import {fromU64Str, toU64Str} from "../../../utils/serdes.js"; import { RoutesData, ReturnTypes, @@ -190,6 +191,30 @@ export type Api = { > >; + /** + * Get validators from state + * Returns filterable list of validators with their balance, status and index. + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + * @param id Either hex encoded public key (with 0x prefix) or validator index + * @param status [Validator status specification](https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ) + */ + postStateValidators( + stateId: StateId, + filters?: ValidatorFilters + ): Promise< + ApiClientResponse< + { + [HttpStatusCode.OK]: { + data: ValidatorResponse[]; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, + HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND + > + >; + /** * Get validator from state by id * Returns validator specified by state and id or public key along with status and balance. @@ -236,6 +261,29 @@ export type Api = { > >; + /** + * Get validator balances from state + * Returns filterable list of validator balances. + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + * @param id Either hex encoded public key (with 0x prefix) or validator index + */ + postStateValidatorBalances( + stateId: StateId, + indices?: ValidatorId[] + ): Promise< + ApiClientResponse< + { + [HttpStatusCode.OK]: { + data: ValidatorBalance[]; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, + HttpStatusCode.BAD_REQUEST + > + >; + /** * Get all committees for a state. * Retrieves the committees for the given state. @@ -290,7 +338,9 @@ export const routesData: RoutesData = { getStateRandao: {url: "/eth/v1/beacon/states/{state_id}/randao", method: "GET"}, getStateValidator: {url: "/eth/v1/beacon/states/{state_id}/validators/{validator_id}", method: "GET"}, getStateValidators: {url: "/eth/v1/beacon/states/{state_id}/validators", method: "GET"}, + postStateValidators: {url: "/eth/v1/beacon/states/{state_id}/validators", method: "POST"}, getStateValidatorBalances: {url: "/eth/v1/beacon/states/{state_id}/validator_balances", method: "GET"}, + postStateValidatorBalances: {url: "/eth/v1/beacon/states/{state_id}/validator_balances", method: "POST"}, }; /* eslint-disable @typescript-eslint/naming-convention */ @@ -306,7 +356,9 @@ export type ReqTypes = { getStateRandao: {params: {state_id: StateId}; query: {epoch?: number}}; getStateValidator: {params: {state_id: StateId; validator_id: ValidatorId}}; getStateValidators: {params: {state_id: StateId}; query: {id?: ValidatorId[]; status?: ValidatorStatus[]}}; + postStateValidators: {params: {state_id: StateId}; body: {ids?: string[]; statuses?: ValidatorStatus[]}}; getStateValidatorBalances: {params: {state_id: StateId}; query: {id?: ValidatorId[]}}; + postStateValidatorBalances: {params: {state_id: StateId}; body?: string[]}; }; export function getReqSerializers(): ReqSerializers { @@ -365,6 +417,27 @@ export function getReqSerializers(): ReqSerializers { }, }, + postStateValidators: { + writeReq: (state_id, filters) => ({ + params: {state_id}, + body: { + ids: filters?.id?.map((id) => (typeof id === "string" ? id : toU64Str(id))), + statuses: filters?.status, + }, + }), + parseReq: ({params, body}) => [ + params.state_id, + { + id: body.ids?.map((id) => (typeof id === "string" && id.startsWith("0x") ? id : fromU64Str(id))), + status: body.statuses, + }, + ], + schema: { + params: {state_id: Schema.StringRequired}, + body: Schema.Object, + }, + }, + getStateValidatorBalances: { writeReq: (state_id, id) => ({params: {state_id}, query: {id}}), parseReq: ({params, query}) => [params.state_id, query.id], @@ -373,6 +446,21 @@ export function getReqSerializers(): ReqSerializers { query: {id: Schema.UintOrStringArray}, }, }, + + postStateValidatorBalances: { + writeReq: (state_id, ids) => ({ + params: {state_id}, + body: ids?.map((id) => (typeof id === "string" ? id : toU64Str(id))) || [], + }), + parseReq: ({params, body}) => [ + params.state_id, + body?.map((id) => (typeof id === "string" && id.startsWith("0x") ? id : fromU64Str(id))), + ], + schema: { + params: {state_id: Schema.StringRequired}, + body: Schema.UintOrStringArray, + }, + }, }; } @@ -435,8 +523,10 @@ export function getReturnTypes(): ReturnTypes { getStateRandao: WithFinalized(ContainerDataExecutionOptimistic(RandaoContainer)), getStateFinalityCheckpoints: WithFinalized(ContainerDataExecutionOptimistic(FinalityCheckpoints)), getStateValidators: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(ValidatorResponse))), + postStateValidators: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(ValidatorResponse))), getStateValidator: WithFinalized(ContainerDataExecutionOptimistic(ValidatorResponse)), getStateValidatorBalances: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(ValidatorBalance))), + postStateValidatorBalances: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(ValidatorBalance))), getEpochCommittees: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(EpochCommitteeResponse))), getEpochSyncCommittees: WithFinalized(ContainerDataExecutionOptimistic(EpochSyncCommitteesResponse)), }; diff --git a/packages/api/test/unit/beacon/oapiSpec.test.ts b/packages/api/test/unit/beacon/oapiSpec.test.ts index 6324a0cd56be..1bffbb486ff7 100644 --- a/packages/api/test/unit/beacon/oapiSpec.test.ts +++ b/packages/api/test/unit/beacon/oapiSpec.test.ts @@ -86,9 +86,6 @@ const testDatas = { const ignoredOperations = [ /* missing route */ - /* https://github.com/ChainSafe/lodestar/issues/6058 */ - "postStateValidators", - "postStateValidatorBalances", "getDepositSnapshot", // Won't fix for now, see https://github.com/ChainSafe/lodestar/issues/5697 "getBlindedBlock", // https://github.com/ChainSafe/lodestar/issues/5699 "getNextWithdrawals", // https://github.com/ChainSafe/lodestar/issues/5696 diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 88894b81d5f0..82e2ae1af421 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -158,6 +158,10 @@ export const testData: GenericServerTestCases = { args: ["head", {id: [pubkeyHex, "1300"], status: ["active_ongoing"]}], res: {executionOptimistic: true, finalized: false, data: [validatorResponse]}, }, + postStateValidators: { + args: ["head", {id: [pubkeyHex, 1300], status: ["active_ongoing"]}], + res: {executionOptimistic: true, finalized: false, data: [validatorResponse]}, + }, getStateValidator: { args: ["head", pubkeyHex], res: {executionOptimistic: true, finalized: false, data: validatorResponse}, @@ -166,6 +170,10 @@ export const testData: GenericServerTestCases = { args: ["head", ["1300"]], res: {executionOptimistic: true, finalized: false, data: [{index: 1300, balance}]}, }, + postStateValidatorBalances: { + args: ["head", [1300]], + res: {executionOptimistic: true, finalized: false, data: [{index: 1300, balance}]}, + }, getEpochCommittees: { args: ["head", {index: 1, slot: 2, epoch: 3}], res: {executionOptimistic: true, finalized: false, data: [{index: 1, slot: 2, validators: [1300]}]}, 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 275b0614af0f..d61d36bf83b6 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/index.ts @@ -134,6 +134,10 @@ export function getBeaconStateApi({ }; }, + async postStateValidators(stateId, filters) { + return this.getStateValidators(stateId, filters); + }, + async getStateValidator(stateId, validatorId) { const {state, executionOptimistic, finalized} = await resolveStateId(chain, stateId); const {pubkey2index} = chain.getHeadState().epochCtx; @@ -195,6 +199,10 @@ export function getBeaconStateApi({ }; }, + async postStateValidatorBalances(stateId, indices) { + return this.getStateValidatorBalances(stateId, indices); + }, + async getEpochCommittees(stateId, filters) { const {state, executionOptimistic, finalized} = await resolveStateId(chain, stateId); 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 5b493868255b..5ed624e9eedf 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/utils.ts @@ -129,37 +129,42 @@ export function filterStateValidatorsByStatus( return responses; } -type StateValidatorIndexResponse = {valid: true; validatorIndex: number} | {valid: false; code: number; reason: string}; +type StateValidatorIndexResponse = + | {valid: true; validatorIndex: ValidatorIndex} + | {valid: false; code: number; reason: string}; export function getStateValidatorIndex( id: routes.beacon.ValidatorId | BLSPubkey, state: BeaconStateAllForks, pubkey2index: PubkeyIndexMap ): StateValidatorIndexResponse { - let validatorIndex: ValidatorIndex | undefined; if (typeof id === "string") { + // mutate `id` and fallthrough to below if (id.startsWith("0x")) { - // mutate `id` and fallthrough to below try { id = fromHexString(id); } catch (e) { return {valid: false, code: 400, reason: "Invalid pubkey hex encoding"}; } } else { - validatorIndex = Number(id); - // validator is invalid or added later than given stateId - if (!Number.isSafeInteger(validatorIndex)) { - return {valid: false, code: 400, reason: "Invalid validator index"}; - } - if (validatorIndex >= state.validators.length) { - return {valid: false, code: 404, reason: "Validator index from future state"}; - } - return {valid: true, validatorIndex}; + id = Number(id); + } + } + + if (typeof id === "number") { + const validatorIndex = id; + // validator is invalid or added later than given stateId + if (!Number.isSafeInteger(validatorIndex)) { + return {valid: false, code: 400, reason: "Invalid validator index"}; + } + if (validatorIndex >= state.validators.length) { + return {valid: false, code: 404, reason: "Validator index from future state"}; } + return {valid: true, validatorIndex}; } // typeof id === Uint8Array - validatorIndex = pubkey2index.get(id as BLSPubkey); + const validatorIndex = pubkey2index.get(id); if (validatorIndex === undefined) { return {valid: false, code: 404, reason: "Validator pubkey not found in state"}; } 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 6986df406bf0..a6020c0a3c13 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 @@ -126,21 +126,27 @@ describe("beacon state api utils", function () { if (resp1.valid) { expect(resp1.validatorIndex).toBe(index); } else { - expect.fail("validator index should be found - validator index input"); + expect.fail("validator index should be found - validator index as string input"); } - const pubkey = state.validators.get(index).pubkey; - const resp2 = getStateValidatorIndex(pubkey, state, pubkey2index); + const resp2 = getStateValidatorIndex(index, state, pubkey2index); if (resp2.valid) { expect(resp2.validatorIndex).toBe(index); } else { - expect.fail("validator index should be found - Uint8Array input"); + expect.fail("validator index should be found - validator index as number input"); } - const resp3 = getStateValidatorIndex(toHexString(pubkey), state, pubkey2index); + const pubkey = state.validators.get(index).pubkey; + const resp3 = getStateValidatorIndex(pubkey, state, pubkey2index); if (resp3.valid) { expect(resp3.validatorIndex).toBe(index); } else { expect.fail("validator index should be found - Uint8Array input"); } + const resp4 = getStateValidatorIndex(toHexString(pubkey), state, pubkey2index); + if (resp4.valid) { + expect(resp4.validatorIndex).toBe(index); + } else { + expect.fail("validator index should be found - Uint8Array input"); + } }); }); }); diff --git a/packages/cli/test/utils/mockBeaconApiServer.ts b/packages/cli/test/utils/mockBeaconApiServer.ts index dc5a88c21746..9aea2c8d2857 100644 --- a/packages/cli/test/utils/mockBeaconApiServer.ts +++ b/packages/cli/test/utils/mockBeaconApiServer.ts @@ -44,6 +44,9 @@ export function getMockBeaconApiServer(opts: RestApiServerOpts, apiOpts?: MockBe async getStateValidators() { return {data: [], executionOptimistic: false, finalized: false}; }, + async postStateValidators() { + return {data: [], executionOptimistic: false, finalized: false}; + }, }, config: {