Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support ssz response for validators endpoint #6060

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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