From 39a55fb6814d134652c6e387e99a90e35d9c731f Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sun, 22 Oct 2023 17:35:31 +0200 Subject: [PATCH] Support ssz response for validators endpoint --- packages/api/src/beacon/client/beacon.ts | 29 ++++++++- .../api/src/beacon/routes/beacon/state.ts | 64 ++++++++++++------- packages/api/src/beacon/server/beacon.ts | 12 ++++ .../api/test/unit/beacon/testData/beacon.ts | 2 +- .../src/api/impl/beacon/state/index.ts | 33 ++++++---- packages/cli/test/sim/endpoints.test.ts | 10 +++ 6 files changed, 111 insertions(+), 39 deletions(-) diff --git a/packages/api/src/beacon/client/beacon.ts b/packages/api/src/beacon/client/beacon.ts index 7a92afe15c6f..53bce39f967f 100644 --- a/packages/api/src/beacon/client/beacon.ts +++ b/packages/api/src/beacon/client/beacon.ts @@ -1,8 +1,18 @@ import {ChainForkConfig} from "@lodestar/config"; -import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes, BlockId} from "../routes/beacon/index.js"; +import { + Api, + ReqTypes, + routesData, + getReqSerializers, + getReturnTypes, + BlockId, + StateId, + ValidatorFilters, +} from "../routes/beacon/index.js"; import {IHttpClient, generateGenericJsonClient, getFetchOptsSerializers} from "../../utils/client/index.js"; import {ResponseFormat} from "../../interfaces.js"; import {BlockResponse, BlockV2Response} from "../routes/beacon/block.js"; +import {ValidatorsResponse} from "../routes/beacon/state.js"; /** * REST HTTP client for beacon routes @@ -16,6 +26,23 @@ export function getClient(config: ChainForkConfig, httpClient: IHttpClient): Api return { ...client, + async getStateValidators( + stateId: StateId, + filters?: ValidatorFilters, + format?: T + ) { + if (format === "ssz") { + const res = await httpClient.arrayBuffer({ + ...fetchOptsSerializer.getStateValidators(stateId, filters, format), + }); + return { + ok: true, + response: new Uint8Array(res.body), + status: res.status, + } as ValidatorsResponse; + } + return client.getStateValidators(stateId, filters, format); + }, async getBlock(blockId: BlockId, format?: T) { if (format === "ssz") { const res = await httpClient.arrayBuffer({ diff --git a/packages/api/src/beacon/routes/beacon/state.ts b/packages/api/src/beacon/routes/beacon/state.ts index 8719ceb44fb9..11ae5a837589 100644 --- a/packages/api/src/beacon/routes/beacon/state.ts +++ b/packages/api/src/beacon/routes/beacon/state.ts @@ -11,7 +11,8 @@ import { ValidatorStatus, validatorStatusType, } from "@lodestar/types"; -import {ApiClientResponse} from "../../../interfaces.js"; +import {ApiClientResponse, ResponseFormat} from "../../../interfaces.js"; +import {parseAcceptHeader, writeAcceptHeader} from "../../../utils/acceptHeader.js"; import {HttpStatusCode} from "../../../utils/client/httpStatusCode.js"; import { RoutesData, @@ -76,6 +77,27 @@ export type EpochSyncCommitteeResponse = { validatorAggregates: ValidatorIndex[][]; }; +export type ValidatorsResponse = T extends "ssz" + ? ApiClientResponse<{[HttpStatusCode.OK]: Uint8Array}, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND> + : ApiClientResponse< + {[HttpStatusCode.OK]: {data: ValidatorResponse[]; executionOptimistic: ExecutionOptimistic}}, + HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND + >; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const ValidatorResponseType = new ContainerType( + { + index: ssz.ValidatorIndex, + balance: ssz.UintNum64, + status: validatorStatusType, + validator: ssz.phase0.Validator, + }, + {jsonCase: "eth2"} +); + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const ValidatorsResponseType = ArrayOf(ValidatorResponseType); + export type Api = { /** * Get state SSZ HashTreeRoot @@ -132,15 +154,11 @@ export type Api = { * @param id Either hex encoded public key (with 0x prefix) or validator index * @param status [Validator status specification](https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ) */ - getStateValidators( + getStateValidators( stateId: StateId, - filters?: ValidatorFilters - ): Promise< - ApiClientResponse< - {[HttpStatusCode.OK]: {data: ValidatorResponse[]; executionOptimistic: ExecutionOptimistic}}, - HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND - > - >; + filters?: ValidatorFilters, + format?: T + ): Promise>; /** * Get validator from state by id @@ -231,7 +249,11 @@ export type ReqTypes = { getStateFork: StateIdOnlyReq; getStateRoot: StateIdOnlyReq; getStateValidator: {params: {state_id: StateId; validator_id: ValidatorId}}; - getStateValidators: {params: {state_id: StateId}; query: {id?: ValidatorId[]; status?: ValidatorStatus[]}}; + getStateValidators: { + params: {state_id: StateId}; + query: {id?: ValidatorId[]; status?: ValidatorStatus[]}; + headers: {accept?: string}; + }; getStateValidatorBalances: {params: {state_id: StateId}; query: {id?: ValidatorId[]}}; }; @@ -274,8 +296,12 @@ export function getReqSerializers(): ReqSerializers { }, getStateValidators: { - writeReq: (state_id, filters) => ({params: {state_id}, query: filters || {}}), - parseReq: ({params, query}) => [params.state_id, query], + writeReq: (state_id, filters, format) => ({ + params: {state_id}, + query: filters || {}, + headers: {accept: writeAcceptHeader(format)}, + }), + parseReq: ({params, query, headers}) => [params.state_id, query, parseAcceptHeader(headers.accept)], schema: { params: {state_id: Schema.StringRequired}, query: {id: Schema.UintOrStringArray, status: Schema.StringArray}, @@ -307,16 +333,6 @@ export function getReturnTypes(): ReturnTypes { {jsonCase: "eth2"} ); - const ValidatorResponse = new ContainerType( - { - index: ssz.ValidatorIndex, - balance: ssz.UintNum64, - status: validatorStatusType, - validator: ssz.phase0.Validator, - }, - {jsonCase: "eth2"} - ); - const ValidatorBalance = new ContainerType( { index: ssz.ValidatorIndex, @@ -346,8 +362,8 @@ export function getReturnTypes(): ReturnTypes { getStateRoot: ContainerDataExecutionOptimistic(RootContainer), getStateFork: ContainerDataExecutionOptimistic(ssz.phase0.Fork), getStateFinalityCheckpoints: ContainerDataExecutionOptimistic(FinalityCheckpoints), - getStateValidators: ContainerDataExecutionOptimistic(ArrayOf(ValidatorResponse)), - getStateValidator: ContainerDataExecutionOptimistic(ValidatorResponse), + getStateValidators: ContainerDataExecutionOptimistic(ArrayOf(ValidatorResponseType)), + getStateValidator: ContainerDataExecutionOptimistic(ValidatorResponseType), getStateValidatorBalances: ContainerDataExecutionOptimistic(ArrayOf(ValidatorBalance)), getEpochCommittees: ContainerDataExecutionOptimistic(ArrayOf(EpochCommitteeResponse)), getEpochSyncCommittees: ContainerDataExecutionOptimistic(EpochSyncCommitteesResponse), diff --git a/packages/api/src/beacon/server/beacon.ts b/packages/api/src/beacon/server/beacon.ts index da5a0997a0d8..5230647c0ac9 100644 --- a/packages/api/src/beacon/server/beacon.ts +++ b/packages/api/src/beacon/server/beacon.ts @@ -16,6 +16,18 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerR return { ...serverRoutes, // Non-JSON routes. Return JSON or binary depending on "accept" header + getStateValidators: { + ...serverRoutes.getStateValidators, + handler: async (req) => { + const response = await api.getStateValidators(...reqSerializers.getStateValidators.parseReq(req)); + if (response instanceof Uint8Array) { + // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer + return Buffer.from(response); + } else { + return returnTypes.getStateValidators.toJson(response); + } + }, + }, getBlock: { ...serverRoutes.getBlock, handler: async (req) => { diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index bb9697cf9587..3e4a704beb9e 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -143,7 +143,7 @@ export const testData: GenericServerTestCases = { }, }, getStateValidators: { - args: ["head", {id: [pubkeyHex, "1300"], status: ["active_ongoing"]}], + args: ["head", {id: [pubkeyHex, "1300"], status: ["active_ongoing"]}, "json"], res: {executionOptimistic: true, data: [validatorResponse]}, }, getStateValidator: { 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 54d663234afe..fbb4b7c4379e 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/index.ts @@ -55,7 +55,7 @@ export function getBeaconStateApi({ }; }, - async getStateValidators(stateId, filters) { + async getStateValidators(stateId, filters, format) { const {state, executionOptimistic} = await resolveStateId(chain, stateId); const currentEpoch = getCurrentEpoch(state); const {validators, balances} = state; // Get the validators sub tree once for all the loop @@ -80,16 +80,21 @@ export function getBeaconStateApi({ validatorResponses.push(validatorResponse); } } - return { - executionOptimistic, - data: validatorResponses, - }; + + return format === "ssz" + ? routes.beacon.state.ValidatorsResponseType.serialize(validatorResponses) + : { + executionOptimistic, + data: validatorResponses, + }; } else if (filters?.status) { const validatorsByStatus = filterStateValidatorsByStatus(filters.status, state, pubkey2index, currentEpoch); - return { - executionOptimistic, - data: validatorsByStatus, - }; + return format === "ssz" + ? routes.beacon.state.ValidatorsResponseType.serialize(validatorsByStatus) + : { + executionOptimistic, + data: validatorsByStatus, + }; } // TODO: This loops over the entire state, it's a DOS vector @@ -100,10 +105,12 @@ export function getBeaconStateApi({ resp.push(toValidatorResponse(i, validatorsArr[i], balancesArr[i], currentEpoch)); } - return { - executionOptimistic, - data: resp, - }; + return format === "ssz" + ? routes.beacon.state.ValidatorsResponseType.serialize(resp) + : { + executionOptimistic, + data: resp, + }; }, async getStateValidator(stateId, validatorId) { diff --git a/packages/cli/test/sim/endpoints.test.ts b/packages/cli/test/sim/endpoints.test.ts index 89d5428057f2..48a35ab29f73 100644 --- a/packages/cli/test/sim/endpoints.test.ts +++ b/packages/cli/test/sim/endpoints.test.ts @@ -94,6 +94,16 @@ await env.tracker.assert( } ); +await env.tracker.assert( + "should return validators as SSZ response when getStateValidators is called with format 'ssz'", + async () => { + const sszRes = await node.api.beacon.getStateValidators("head", {}, "ssz"); + ApiError.assert(sszRes); + const sszStateValidators = routes.beacon.state.ValidatorsResponseType.deserialize(sszRes.response); + expect(sszStateValidators).to.deep.equal(stateValidators); + } +); + await env.tracker.assert( "should return the validator when getStateValidator is called with the validator index", async () => {