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: add POST endpoints for validators and validator_balances #6655

Merged
merged 2 commits into from
Apr 11, 2024
Merged
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
90 changes: 90 additions & 0 deletions packages/api/src/beacon/routes/beacon/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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", \<slot\>, \<hex encoded stateRoot with 0x prefix\>.
* @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.
Expand Down Expand Up @@ -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", \<slot\>, \<hex encoded stateRoot with 0x prefix\>.
* @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.
Expand Down Expand Up @@ -290,7 +338,9 @@ export const routesData: RoutesData<Api> = {
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 */
Expand All @@ -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<Api, ReqTypes> {
Expand Down Expand Up @@ -365,6 +417,27 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
},
},

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))),
jeluard marked this conversation as resolved.
Show resolved Hide resolved
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],
Expand All @@ -373,6 +446,21 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
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))),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id is always a string? fromU64Str only accepts string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, if we use string for validator index inside Lodestar it is likely unintended and should be fixed. And even though the type of fromU64Str states in only accepts strings (which imo it should), in practice it will handle numbers as type number just fine.

It only has to be a stringified number over the wire

apis/beacon/states/validators.yaml#L115-L120

ids:
 type: array
 uniqueItems: true
 items:
   description: "Either hex encoded public key (any bytes48 with 0x prefix) or validator index"
   type: string # <--

Copy link
Member Author

@nflaig nflaig Apr 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indices store strictly uses type number

export class IndicesService {
readonly index2pubkey = new Map<ValidatorIndex, PubkeyHex>();
/** Indexed by pubkey in hex 0x prefixed */
readonly pubkey2index = new Map<PubkeyHex, ValidatorIndex>();

],
schema: {
params: {state_id: Schema.StringRequired},
body: Schema.UintOrStringArray,
},
},
};
}

Expand Down Expand Up @@ -435,8 +523,10 @@ export function getReturnTypes(): ReturnTypes<Api> {
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)),
};
Expand Down
3 changes: 0 additions & 3 deletions packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ export const testData: GenericServerTestCases<Api> = {
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},
Expand All @@ -166,6 +170,10 @@ export const testData: GenericServerTestCases<Api> = {
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]}]},
Expand Down
8 changes: 8 additions & 0 deletions packages/beacon-node/src/api/impl/beacon/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
31 changes: 18 additions & 13 deletions packages/beacon-node/src/api/impl/beacon/state/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
});
});
});
3 changes: 3 additions & 0 deletions packages/cli/test/utils/mockBeaconApiServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading