Skip to content

Commit

Permalink
feat: add POST endpoints for validators and validator_balances (#6655)
Browse files Browse the repository at this point in the history
* feat: add POST endpoints for validators and validator_balances

* Update getStateValidatorIndex
  • Loading branch information
nflaig authored Apr 11, 2024
1 parent 5ccae1c commit 669239b
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 21 deletions.
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))),
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))),
],
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
16 changes: 11 additions & 5 deletions packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts
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

0 comments on commit 669239b

Please sign in to comment.