Skip to content

Commit

Permalink
Support ssz response for validators endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
nflaig committed Oct 23, 2023
1 parent f46e2b6 commit 39a55fb
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 39 deletions.
29 changes: 28 additions & 1 deletion packages/api/src/beacon/client/beacon.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,6 +26,23 @@ export function getClient(config: ChainForkConfig, httpClient: IHttpClient): Api

return {
...client,
async getStateValidators<T extends ResponseFormat = "json">(
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<T>;
}
return client.getStateValidators(stateId, filters, format);
},
async getBlock<T extends ResponseFormat = "json">(blockId: BlockId, format?: T) {
if (format === "ssz") {
const res = await httpClient.arrayBuffer({
Expand Down
64 changes: 40 additions & 24 deletions packages/api/src/beacon/routes/beacon/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -76,6 +77,27 @@ export type EpochSyncCommitteeResponse = {
validatorAggregates: ValidatorIndex[][];
};

export type ValidatorsResponse<T extends ResponseFormat = "json"> = 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
Expand Down Expand Up @@ -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<T extends ResponseFormat = "json">(
stateId: StateId,
filters?: ValidatorFilters
): Promise<
ApiClientResponse<
{[HttpStatusCode.OK]: {data: ValidatorResponse[]; executionOptimistic: ExecutionOptimistic}},
HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND
>
>;
filters?: ValidatorFilters,
format?: T
): Promise<ValidatorsResponse<T>>;

/**
* Get validator from state by id
Expand Down Expand Up @@ -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[]}};
};

Expand Down Expand Up @@ -274,8 +296,12 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
},

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},
Expand Down Expand Up @@ -307,16 +333,6 @@ export function getReturnTypes(): ReturnTypes<Api> {
{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,
Expand Down Expand Up @@ -346,8 +362,8 @@ export function getReturnTypes(): ReturnTypes<Api> {
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),
Expand Down
12 changes: 12 additions & 0 deletions packages/api/src/beacon/server/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi<Api>): 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) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export const testData: GenericServerTestCases<Api> = {
},
},
getStateValidators: {
args: ["head", {id: [pubkeyHex, "1300"], status: ["active_ongoing"]}],
args: ["head", {id: [pubkeyHex, "1300"], status: ["active_ongoing"]}, "json"],
res: {executionOptimistic: true, data: [validatorResponse]},
},
getStateValidator: {
Expand Down
33 changes: 20 additions & 13 deletions packages/beacon-node/src/api/impl/beacon/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/test/sim/endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down

0 comments on commit 39a55fb

Please sign in to comment.