Skip to content

Commit

Permalink
feat: add validator identities endpoint (#7107)
Browse files Browse the repository at this point in the history
* feat: add validator identities endpoint

* Test against official spec release from beacon-apis repo

* Prepopulate validatorIdentities in else clause

* Log warning if validator index can't be resolved
  • Loading branch information
nflaig authored Sep 27, 2024
1 parent 94b6c5b commit f87ee1a
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/api/src/beacon/routes/beacon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type {
export type {
StateId,
ValidatorId,
ValidatorIdentities,
ValidatorStatus,
FinalityCheckpoints,
ValidatorResponse,
Expand Down
52 changes: 52 additions & 0 deletions packages/api/src/beacon/routes/beacon/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export const ValidatorResponseType = new ContainerType({
status: new StringType<ValidatorStatus>(),
validator: ssz.phase0.Validator,
});
export const ValidatorIdentityType = new ContainerType(
{
index: ssz.ValidatorIndex,
pubkey: ssz.BLSPubkey,
activationEpoch: ssz.UintNum64,
},
{jsonCase: "eth2"}
);
export const EpochCommitteeResponseType = new ContainerType({
index: ssz.CommitteeIndex,
slot: ssz.Slot,
Expand All @@ -73,6 +81,7 @@ export const EpochSyncCommitteeResponseType = new ContainerType(
{jsonCase: "eth2"}
);
export const ValidatorResponseListType = ArrayOf(ValidatorResponseType);
export const ValidatorIdentitiesType = ArrayOf(ValidatorIdentityType);
export const EpochCommitteeResponseListType = ArrayOf(EpochCommitteeResponseType);
export const ValidatorBalanceListType = ArrayOf(ValidatorBalanceType);

Expand All @@ -84,6 +93,7 @@ export type ValidatorBalance = ValueOf<typeof ValidatorBalanceType>;
export type EpochSyncCommitteeResponse = ValueOf<typeof EpochSyncCommitteeResponseType>;

export type ValidatorResponseList = ValueOf<typeof ValidatorResponseListType>;
export type ValidatorIdentities = ValueOf<typeof ValidatorIdentitiesType>;
export type EpochCommitteeResponseList = ValueOf<typeof EpochCommitteeResponseListType>;
export type ValidatorBalanceList = ValueOf<typeof ValidatorBalanceListType>;

Expand Down Expand Up @@ -191,6 +201,26 @@ export type Endpoints = {
ExecutionOptimisticAndFinalizedMeta
>;

/**
* Get validator identities from state
*
* Returns filterable list of validators identities.
*
* Identities will be returned for all indices or public keys that match known validators. If an index or public key does not
* match any known validator, no identity will be returned but this will not cause an error. There are no guarantees for the
* returned data in terms of ordering.
*/
postStateValidatorIdentities: Endpoint<
"POST",
StateArgs & {
/** An array of values, with each value either a hex encoded public key (any bytes48 with 0x prefix) or a validator index */
validatorIds?: ValidatorId[];
},
{params: {state_id: string}; body: string[]},
ValidatorIdentities,
ExecutionOptimisticAndFinalizedMeta
>;

/**
* Get validator balances from state
* Returns filterable list of validator balances.
Expand Down Expand Up @@ -404,6 +434,28 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
meta: ExecutionOptimisticAndFinalizedCodec,
},
},
postStateValidatorIdentities: {
url: "/eth/v1/beacon/states/{state_id}/validator_identities",
method: "POST",
req: JsonOnlyReq({
writeReqJson: ({stateId, validatorIds}) => ({
params: {state_id: stateId.toString()},
body: toValidatorIdsStr(validatorIds) || [],
}),
parseReqJson: ({params, body = []}) => ({
stateId: params.state_id,
validatorIds: fromValidatorIdsStr(body),
}),
schema: {
params: {state_id: Schema.StringRequired},
body: Schema.UintOrStringArray,
},
}),
resp: {
data: ValidatorIdentitiesType,
meta: ExecutionOptimisticAndFinalizedCodec,
},
},
getStateValidatorBalances: {
url: "/eth/v1/beacon/states/{state_id}/validator_balances",
method: "GET",
Expand Down
4 changes: 2 additions & 2 deletions packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import {testData as validatorTestData} from "./testData/validator.js";
// eslint-disable-next-line @typescript-eslint/naming-convention
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const version = "v2.6.0-alpha.1";
const version = "v3.0.0-alpha.6";
const openApiFile: OpenApiFile = {
url: `https://raw.githubusercontent.com/nflaig/beacon-api-spec/main/${version}/beacon-node-oapi.json`,
url: `https://github.com/ethereum/beacon-APIs/releases/download/${version}/beacon-node-oapi.json`,
filepath: path.join(__dirname, "../../../oapi-schemas/beacon-node-oapi.json"),
version: RegExp(version),
};
Expand Down
7 changes: 7 additions & 0 deletions packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ export const testData: GenericServerTestCases<Endpoints> = {
args: {stateId: "head", validatorIds: [pubkeyHex, 1300], statuses: ["active_ongoing"]},
res: {data: [validatorResponse], meta: {executionOptimistic: true, finalized: false}},
},
postStateValidatorIdentities: {
args: {stateId: "head", validatorIds: [1300]},
res: {
data: [{index: 1300, pubkey: ssz.BLSPubkey.defaultValue(), activationEpoch: 1}],
meta: {executionOptimistic: true, finalized: false},
},
},
getStateValidator: {
args: {stateId: "head", validatorId: pubkeyHex},
res: {data: validatorResponse, meta: {executionOptimistic: true, finalized: false}},
Expand Down
38 changes: 37 additions & 1 deletion packages/beacon-node/src/api/impl/beacon/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
export function getBeaconStateApi({
chain,
config,
}: Pick<ApiModules, "chain" | "config">): ApplicationMethods<routes.beacon.state.Endpoints> {
logger,
}: Pick<ApiModules, "chain" | "config" | "logger">): ApplicationMethods<routes.beacon.state.Endpoints> {
async function getState(
stateId: routes.beacon.StateId
): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean}> {
Expand Down Expand Up @@ -98,6 +99,8 @@ export function getBeaconStateApi({
currentEpoch
);
validatorResponses.push(validatorResponse);
} else {
logger.warn(resp.reason, {id});
}
}
return {
Expand Down Expand Up @@ -130,6 +133,39 @@ export function getBeaconStateApi({
return this.getStateValidators(args, context);
},

async postStateValidatorIdentities({stateId, validatorIds = []}) {
const {state, executionOptimistic, finalized} = await getStateResponse(chain, stateId);
const {pubkey2index} = chain.getHeadState().epochCtx;

let validatorIdentities: routes.beacon.ValidatorIdentities;

if (validatorIds.length) {
validatorIdentities = [];
for (const id of validatorIds) {
const resp = getStateValidatorIndex(id, state, pubkey2index);
if (resp.valid) {
const index = resp.validatorIndex;
const {pubkey, activationEpoch} = state.validators.getReadonly(index);
validatorIdentities.push({index, pubkey, activationEpoch});
} else {
logger.warn(resp.reason, {id});
}
}
} else {
const validatorsArr = state.validators.getAllReadonlyValues();
validatorIdentities = new Array(validatorsArr.length) as routes.beacon.ValidatorIdentities;
for (let i = 0; i < validatorsArr.length; i++) {
const {pubkey, activationEpoch} = validatorsArr[i];
validatorIdentities[i] = {index: i, pubkey, activationEpoch};
}
}

return {
data: validatorIdentities,
meta: {executionOptimistic, finalized},
};
},

async getStateValidator({stateId, validatorId}) {
const {state, executionOptimistic, finalized} = await getStateResponse(chain, stateId);
const {pubkey2index} = chain.getHeadState().epochCtx;
Expand Down

0 comments on commit f87ee1a

Please sign in to comment.