Skip to content

Commit f87ee1a

Browse files
authored
feat: add validator identities endpoint (#7107)
* 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
1 parent 94b6c5b commit f87ee1a

File tree

5 files changed

+99
-3
lines changed

5 files changed

+99
-3
lines changed

packages/api/src/beacon/routes/beacon/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type {
2525
export type {
2626
StateId,
2727
ValidatorId,
28+
ValidatorIdentities,
2829
ValidatorStatus,
2930
FinalityCheckpoints,
3031
ValidatorResponse,

packages/api/src/beacon/routes/beacon/state.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ export const ValidatorResponseType = new ContainerType({
5353
status: new StringType<ValidatorStatus>(),
5454
validator: ssz.phase0.Validator,
5555
});
56+
export const ValidatorIdentityType = new ContainerType(
57+
{
58+
index: ssz.ValidatorIndex,
59+
pubkey: ssz.BLSPubkey,
60+
activationEpoch: ssz.UintNum64,
61+
},
62+
{jsonCase: "eth2"}
63+
);
5664
export const EpochCommitteeResponseType = new ContainerType({
5765
index: ssz.CommitteeIndex,
5866
slot: ssz.Slot,
@@ -73,6 +81,7 @@ export const EpochSyncCommitteeResponseType = new ContainerType(
7381
{jsonCase: "eth2"}
7482
);
7583
export const ValidatorResponseListType = ArrayOf(ValidatorResponseType);
84+
export const ValidatorIdentitiesType = ArrayOf(ValidatorIdentityType);
7685
export const EpochCommitteeResponseListType = ArrayOf(EpochCommitteeResponseType);
7786
export const ValidatorBalanceListType = ArrayOf(ValidatorBalanceType);
7887

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

8695
export type ValidatorResponseList = ValueOf<typeof ValidatorResponseListType>;
96+
export type ValidatorIdentities = ValueOf<typeof ValidatorIdentitiesType>;
8797
export type EpochCommitteeResponseList = ValueOf<typeof EpochCommitteeResponseListType>;
8898
export type ValidatorBalanceList = ValueOf<typeof ValidatorBalanceListType>;
8999

@@ -191,6 +201,26 @@ export type Endpoints = {
191201
ExecutionOptimisticAndFinalizedMeta
192202
>;
193203

204+
/**
205+
* Get validator identities from state
206+
*
207+
* Returns filterable list of validators identities.
208+
*
209+
* Identities will be returned for all indices or public keys that match known validators. If an index or public key does not
210+
* match any known validator, no identity will be returned but this will not cause an error. There are no guarantees for the
211+
* returned data in terms of ordering.
212+
*/
213+
postStateValidatorIdentities: Endpoint<
214+
"POST",
215+
StateArgs & {
216+
/** An array of values, with each value either a hex encoded public key (any bytes48 with 0x prefix) or a validator index */
217+
validatorIds?: ValidatorId[];
218+
},
219+
{params: {state_id: string}; body: string[]},
220+
ValidatorIdentities,
221+
ExecutionOptimisticAndFinalizedMeta
222+
>;
223+
194224
/**
195225
* Get validator balances from state
196226
* Returns filterable list of validator balances.
@@ -404,6 +434,28 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
404434
meta: ExecutionOptimisticAndFinalizedCodec,
405435
},
406436
},
437+
postStateValidatorIdentities: {
438+
url: "/eth/v1/beacon/states/{state_id}/validator_identities",
439+
method: "POST",
440+
req: JsonOnlyReq({
441+
writeReqJson: ({stateId, validatorIds}) => ({
442+
params: {state_id: stateId.toString()},
443+
body: toValidatorIdsStr(validatorIds) || [],
444+
}),
445+
parseReqJson: ({params, body = []}) => ({
446+
stateId: params.state_id,
447+
validatorIds: fromValidatorIdsStr(body),
448+
}),
449+
schema: {
450+
params: {state_id: Schema.StringRequired},
451+
body: Schema.UintOrStringArray,
452+
},
453+
}),
454+
resp: {
455+
data: ValidatorIdentitiesType,
456+
meta: ExecutionOptimisticAndFinalizedCodec,
457+
},
458+
},
407459
getStateValidatorBalances: {
408460
url: "/eth/v1/beacon/states/{state_id}/validator_balances",
409461
method: "GET",

packages/api/test/unit/beacon/oapiSpec.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import {testData as validatorTestData} from "./testData/validator.js";
2121
// eslint-disable-next-line @typescript-eslint/naming-convention
2222
const __dirname = path.dirname(fileURLToPath(import.meta.url));
2323

24-
const version = "v2.6.0-alpha.1";
24+
const version = "v3.0.0-alpha.6";
2525
const openApiFile: OpenApiFile = {
26-
url: `https://raw.githubusercontent.com/nflaig/beacon-api-spec/main/${version}/beacon-node-oapi.json`,
26+
url: `https://github.com/ethereum/beacon-APIs/releases/download/${version}/beacon-node-oapi.json`,
2727
filepath: path.join(__dirname, "../../../oapi-schemas/beacon-node-oapi.json"),
2828
version: RegExp(version),
2929
};

packages/api/test/unit/beacon/testData/beacon.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ export const testData: GenericServerTestCases<Endpoints> = {
191191
args: {stateId: "head", validatorIds: [pubkeyHex, 1300], statuses: ["active_ongoing"]},
192192
res: {data: [validatorResponse], meta: {executionOptimistic: true, finalized: false}},
193193
},
194+
postStateValidatorIdentities: {
195+
args: {stateId: "head", validatorIds: [1300]},
196+
res: {
197+
data: [{index: 1300, pubkey: ssz.BLSPubkey.defaultValue(), activationEpoch: 1}],
198+
meta: {executionOptimistic: true, finalized: false},
199+
},
200+
},
194201
getStateValidator: {
195202
args: {stateId: "head", validatorId: pubkeyHex},
196203
res: {data: validatorResponse, meta: {executionOptimistic: true, finalized: false}},

packages/beacon-node/src/api/impl/beacon/state/index.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
export function getBeaconStateApi({
2323
chain,
2424
config,
25-
}: Pick<ApiModules, "chain" | "config">): ApplicationMethods<routes.beacon.state.Endpoints> {
25+
logger,
26+
}: Pick<ApiModules, "chain" | "config" | "logger">): ApplicationMethods<routes.beacon.state.Endpoints> {
2627
async function getState(
2728
stateId: routes.beacon.StateId
2829
): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean}> {
@@ -98,6 +99,8 @@ export function getBeaconStateApi({
9899
currentEpoch
99100
);
100101
validatorResponses.push(validatorResponse);
102+
} else {
103+
logger.warn(resp.reason, {id});
101104
}
102105
}
103106
return {
@@ -130,6 +133,39 @@ export function getBeaconStateApi({
130133
return this.getStateValidators(args, context);
131134
},
132135

136+
async postStateValidatorIdentities({stateId, validatorIds = []}) {
137+
const {state, executionOptimistic, finalized} = await getStateResponse(chain, stateId);
138+
const {pubkey2index} = chain.getHeadState().epochCtx;
139+
140+
let validatorIdentities: routes.beacon.ValidatorIdentities;
141+
142+
if (validatorIds.length) {
143+
validatorIdentities = [];
144+
for (const id of validatorIds) {
145+
const resp = getStateValidatorIndex(id, state, pubkey2index);
146+
if (resp.valid) {
147+
const index = resp.validatorIndex;
148+
const {pubkey, activationEpoch} = state.validators.getReadonly(index);
149+
validatorIdentities.push({index, pubkey, activationEpoch});
150+
} else {
151+
logger.warn(resp.reason, {id});
152+
}
153+
}
154+
} else {
155+
const validatorsArr = state.validators.getAllReadonlyValues();
156+
validatorIdentities = new Array(validatorsArr.length) as routes.beacon.ValidatorIdentities;
157+
for (let i = 0; i < validatorsArr.length; i++) {
158+
const {pubkey, activationEpoch} = validatorsArr[i];
159+
validatorIdentities[i] = {index: i, pubkey, activationEpoch};
160+
}
161+
}
162+
163+
return {
164+
data: validatorIdentities,
165+
meta: {executionOptimistic, finalized},
166+
};
167+
},
168+
133169
async getStateValidator({stateId, validatorId}) {
134170
const {state, executionOptimistic, finalized} = await getStateResponse(chain, stateId);
135171
const {pubkey2index} = chain.getHeadState().epochCtx;

0 commit comments

Comments
 (0)