Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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))),
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
26 changes: 15 additions & 11 deletions packages/beacon-node/src/api/impl/beacon/state/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,28 +138,32 @@ export function getStateValidatorIndex(
): 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") {
validatorIndex = id;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Having 2 declaration for validatorIndex with smaller scope would improve readability and prevent the issue you fixed.

Copy link
Copy Markdown
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.

The issue was really there because of a wrong type cast, so by removing that it should not be possible anymore. But your suggestion makes sense, simpler to follow state of validatorIndex, as it really doesn't need to be declared at the top of the function

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

also added unit test to make sure we would catch any regression

// 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);
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