Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/api/src/beacon/routes/beacon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type {BlockHeaderResponse, BlockId} from "./block.js";
export {BroadcastValidation} from "./block.js";
// TODO: Review if re-exporting all these types is necessary
export type {
BuilderResponse,
BuilderStatus,
EpochCommitteeResponse,
EpochSyncCommitteeResponse,
FinalityCheckpoints,
Expand Down
62 changes: 61 additions & 1 deletion packages/api/src/beacon/routes/beacon/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {ChainForkConfig} from "@lodestar/config";
import {MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params";
import {
ArrayOf,
BuilderStatus,
CommitteeIndex,
Epoch,
RootHex,
Expand Down Expand Up @@ -38,7 +39,7 @@ export type StateArgs = {

export type ValidatorId = string | number;

export type {ValidatorStatus};
export type {BuilderStatus, ValidatorStatus};

export const RandaoResponseType = new ContainerType({
randao: ssz.Root,
Expand All @@ -57,6 +58,11 @@ export const ValidatorResponseType = new ContainerType({
status: new StringType<ValidatorStatus>(),
validator: ssz.phase0.Validator,
});
export const BuilderResponseType = new ContainerType({
index: ssz.BuilderIndex,
status: new StringType<BuilderStatus>(),
builder: ssz.gloas.Builder,
});
export const ValidatorIdentityType = new ContainerType(
{
index: ssz.ValidatorIndex,
Expand Down Expand Up @@ -84,18 +90,21 @@ export const EpochSyncCommitteeResponseType = new ContainerType(
{jsonCase: "eth2"}
);
export const ValidatorResponseListType = ArrayOf(ValidatorResponseType);
export const BuilderResponseListType = ArrayOf(BuilderResponseType);
export const ValidatorIdentitiesType = ArrayOf(ValidatorIdentityType);
export const EpochCommitteeResponseListType = ArrayOf(EpochCommitteeResponseType);
export const ValidatorBalanceListType = ArrayOf(ValidatorBalanceType);

export type RandaoResponse = ValueOf<typeof RandaoResponseType>;
export type FinalityCheckpoints = ValueOf<typeof FinalityCheckpointsType>;
export type ValidatorResponse = ValueOf<typeof ValidatorResponseType>;
export type BuilderResponse = ValueOf<typeof BuilderResponseType>;
export type EpochCommitteeResponse = ValueOf<typeof EpochCommitteeResponseType>;
export type ValidatorBalance = ValueOf<typeof ValidatorBalanceType>;
export type EpochSyncCommitteeResponse = ValueOf<typeof EpochSyncCommitteeResponseType>;

export type ValidatorResponseList = ValueOf<typeof ValidatorResponseListType>;
export type BuilderResponseList = ValueOf<typeof BuilderResponseListType>;
export type ValidatorIdentities = ValueOf<typeof ValidatorIdentitiesType>;
export type EpochCommitteeResponseList = ValueOf<typeof EpochCommitteeResponseListType>;
export type ValidatorBalanceList = ValueOf<typeof ValidatorBalanceListType>;
Expand Down Expand Up @@ -204,6 +213,30 @@ export type Endpoints = {
ExecutionOptimisticAndFinalizedMeta
>;

/**
* Get builders from state
*
* Returns filterable list of builders with their status and index.
*
* Information will be returned for all indices or public keys that match known builders. If an index or public key does not
* match any known builder, no information will be returned but this will not cause an error. There are no guarantees for the
* returned data in terms of ordering; both the index and public key are returned for each builder, and can be used to confirm
* for which inputs a response has been returned.
*
* Returns 400 if the requested state is prior to Gloas.
*/
getStateBuilders: Endpoint<
"POST",
StateArgs & {
/** Either hex encoded public key (any bytes48 with 0x prefix) or builder index */
builderIds?: ValidatorId[];
statuses?: BuilderStatus[];
},
{params: {state_id: string}; body: {ids?: string[]; statuses?: BuilderStatus[]}},
BuilderResponseList,
ExecutionOptimisticAndFinalizedMeta
>;

/**
* Get validator identities from state
*
Expand Down Expand Up @@ -489,6 +522,33 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
meta: ExecutionOptimisticAndFinalizedCodec,
},
},
getStateBuilders: {
url: "/eth/v1/beacon/states/{state_id}/builders",
method: "POST",
req: JsonOnlyReq({
writeReqJson: ({stateId, builderIds, statuses}) => ({
params: {state_id: stateId.toString()},
body: {
ids: toValidatorIdsStr(builderIds),
statuses,
},
}),
parseReqJson: ({params, body = {}}) => ({
stateId: params.state_id,
builderIds: fromValidatorIdsStr(body.ids),
statuses: body.statuses ?? undefined,
}),
schema: {
params: {state_id: Schema.StringRequired},
body: Schema.Object,
},
}),
resp: {
onlySupport: WireFormat.json,
data: BuilderResponseListType,
meta: ExecutionOptimisticAndFinalizedCodec,
},
},
postStateValidatorIdentities: {
url: "/eth/v1/beacon/states/{state_id}/validator_identities",
method: "POST",
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 @@ -205,6 +205,13 @@ export const testData: GenericServerTestCases<Endpoints> = {
args: {stateId: "head", validatorIds: [pubkeyHex, 1300], statuses: ["active_ongoing"]},
res: {data: [validatorResponse], meta: {executionOptimistic: true, finalized: false}},
},
getStateBuilders: {
args: {stateId: "head", builderIds: [pubkeyHex, 32], statuses: ["active"]},
res: {
data: [{index: 32, status: "active", builder: ssz.gloas.Builder.defaultValue()}],
meta: {executionOptimistic: true, finalized: false},
},
},
postStateValidatorIdentities: {
args: {stateId: "head", validatorIds: [1300]},
res: {
Expand Down
53 changes: 52 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 @@ -9,13 +9,15 @@ import {
isStatePostAltair,
isStatePostElectra,
isStatePostFulu,
isStatePostGloas,
} from "@lodestar/state-transition";
import {ValidatorIndex, getValidatorStatus, ssz} from "@lodestar/types";
import {ValidatorIndex, getBuilderStatus, getValidatorStatus, ssz} from "@lodestar/types";
import {ApiError} from "../../errors.js";
import {ApiModules} from "../../types.js";
import {assertUniqueItems} from "../../utils.js";
import {
filterStateValidatorsByStatus,
getStateBuilderIndex,
getStateResponseWithRegen,
getStateValidatorIndex,
toValidatorResponse,
Expand Down Expand Up @@ -143,6 +145,55 @@ export function getBeaconStateApi({
return this.getStateValidators(args, context);
},

async getStateBuilders({stateId, builderIds = [], statuses = []}) {
const {state, executionOptimistic, finalized} = await getState(stateId);
if (!isStatePostGloas(state)) {
throw new ApiError(400, `Builders are not supported for pre-gloas state fork=${state.forkName}`);
}
const finalizedEpoch = state.finalizedCheckpoint.epoch;

const builderResponses: routes.beacon.BuilderResponse[] = [];
if (builderIds.length) {
assertUniqueItems(builderIds, "Duplicate builder IDs provided");
Comment thread
nflaig marked this conversation as resolved.

for (const id of builderIds) {
const resp = getStateBuilderIndex(id, state);
if (resp.valid) {
Comment thread
nflaig marked this conversation as resolved.
const builderIndex = resp.builderIndex;
const builder = state.getBuilder(builderIndex);
const status = getBuilderStatus(builder, finalizedEpoch);
if (statuses.length && !statuses.includes(status)) {
continue;
}
builderResponses.push({index: builderIndex, status, builder});
}
}
return {
data: builderResponses,
meta: {executionOptimistic, finalized},
};
}

if (statuses.length) {
assertUniqueItems(statuses, "Duplicate statuses provided");
}
Comment thread
nflaig marked this conversation as resolved.

const buildersLength = state.getBuildersLength();
for (let builderIndex = 0; builderIndex < buildersLength; builderIndex++) {
const builder = state.getBuilder(builderIndex);
const status = getBuilderStatus(builder, finalizedEpoch);
if (statuses.length && !statuses.includes(status)) {
continue;
}
builderResponses.push({index: builderIndex, status, builder});
}

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

async postStateValidatorIdentities({stateId, validatorIds = []}) {
const {state, executionOptimistic, finalized} = await getState(stateId);
const {pubkeyCache} = chain;
Expand Down
61 changes: 57 additions & 4 deletions packages/beacon-node/src/api/impl/beacon/state/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import {routes} from "@lodestar/api";
import {CheckpointWithHex, IForkChoice} from "@lodestar/fork-choice";
import {GENESIS_SLOT} from "@lodestar/params";
import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition";
import {BLSPubkey, Epoch, RootHex, Slot, ValidatorIndex, getValidatorStatus, phase0} from "@lodestar/types";
import {fromHex} from "@lodestar/utils";
import {IBeaconStateView, IBeaconStateViewGloas, PubkeyCache} from "@lodestar/state-transition";
import {
BLSPubkey,
BuilderIndex,
Epoch,
RootHex,
Slot,
ValidatorIndex,
getValidatorStatus,
phase0,
} from "@lodestar/types";
import {byteArrayEquals, fromHex} from "@lodestar/utils";
import {IBeaconChain} from "../../../../chain/index.js";
import {ApiError, ValidationError} from "../../errors.js";

Expand Down Expand Up @@ -78,6 +87,50 @@ export function toValidatorResponse(
};
}

type StateBuilderIndexResponse =
| {valid: true; builderIndex: BuilderIndex}
| {valid: false; code: number; reason: string};

export function getStateBuilderIndex(
id: routes.beacon.ValidatorId | BLSPubkey,
state: IBeaconStateViewGloas
): StateBuilderIndexResponse {
if (typeof id === "string") {
// mutate `id` and fallthrough to below
if (id.startsWith("0x")) {
try {
id = fromHex(id);
} catch (_e) {
return {valid: false, code: 400, reason: "Invalid pubkey hex encoding"};
}
} else {
id = Number(id);
}
}

if (typeof id === "number") {
const builderIndex = id;
// builder is invalid or added later than given stateId
if (!Number.isSafeInteger(builderIndex) || builderIndex < 0) {
return {valid: false, code: 400, reason: "Invalid builder index"};
}
if (builderIndex >= state.getBuildersLength()) {
return {valid: false, code: 404, reason: "Builder index from future state"};
}
return {valid: true, builderIndex};
}

// typeof id === Uint8Array
// There is no builder pubkey cache, linear scan over the registry
const buildersLength = state.getBuildersLength();
for (let builderIndex = 0; builderIndex < buildersLength; builderIndex++) {
if (byteArrayEquals(state.getBuilder(builderIndex).pubkey, id)) {
return {valid: true, builderIndex};
}
}
return {valid: false, code: 404, reason: "Builder pubkey not found in state"};
}

export function filterStateValidatorsByStatus(
statuses: string[],
state: IBeaconStateView,
Expand Down Expand Up @@ -122,7 +175,7 @@ export function getStateValidatorIndex(
if (typeof id === "number") {
const validatorIndex = id;
// validator is invalid or added later than given stateId
if (!Number.isSafeInteger(validatorIndex)) {
if (!Number.isSafeInteger(validatorIndex) || validatorIndex < 0) {
return {valid: false, code: 400, reason: "Invalid validator index"};
}
if (validatorIndex >= state.validatorCount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ describe("beacon state api utils", () => {
expect(getStateValidatorIndex("foo", state, pubkeyCache).valid).toBe(false);
// "invalid hex"
expect(getStateValidatorIndex("0xfoo", state, pubkeyCache).valid).toBe(false);
// "negative validator index"
expect(getStateValidatorIndex("-1", state, pubkeyCache).valid).toBe(false);
expect(getStateValidatorIndex(-1, state, pubkeyCache).valid).toBe(false);
});

it("should return valid: false on validator indices / pubkeys not in the state", () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/state-transition/src/stateView/beaconStateView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,14 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
return (this.cachedState as CachedBeaconStateGloas).builders.getReadonly(index);
}

getBuildersLength(): number {
if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) {
throw new Error("Builders are not supported before Gloas");
}

return (this.cachedState as CachedBeaconStateGloas).builders.length;
}

canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean {
if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) {
throw new Error("Builders are not supported before Gloas");
Expand Down
1 change: 1 addition & 0 deletions packages/state-transition/src/stateView/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu {
latestExecutionPayloadBid: ExecutionPayloadBid;
payloadExpectedWithdrawals: capella.Withdrawal[];
getBuilder(index: BuilderIndex): gloas.Builder;
getBuildersLength(): number;
canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean;
getEpochPTCs(epoch: Epoch): Uint32Array[];
getIndicesInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export class NativeBeaconStateView implements IBeaconStateViewLatestFork {
private _getNextShuffling: EpochShuffling | null = null;
private _getEffectiveBalanceIncrementsZeroInactive: EffectiveBalanceIncrements | null = null;
private _getAllValidators: phase0.Validator[] | null = null;
private _getBuildersLength: number | null = null;
private _getAllBalances: number[] | null = null;
private _getLatestWeakSubjectivityCheckpointEpoch: Epoch | null = null;
private _getFinalizedRootProof: Uint8Array[] | null = null;
Expand Down Expand Up @@ -888,6 +889,13 @@ export class NativeBeaconStateView implements IBeaconStateViewLatestFork {
return cached;
}

getBuildersLength(): number {
if (this._getBuildersLength === null) {
this._getBuildersLength = this.binding.getBuildersLength();
}
return this._getBuildersLength;
}

canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean {
return this.binding.canBuilderCoverBid(builderIndex, bidAmount);
}
Expand Down
21 changes: 20 additions & 1 deletion packages/types/src/utils/validatorStatus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {FAR_FUTURE_EPOCH} from "@lodestar/params";
import {Epoch, phase0} from "../types.js";
import {Epoch, gloas, phase0} from "../types.js";

/**
* [Validator status specification](https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ)
Expand Down Expand Up @@ -53,6 +53,25 @@ export function getValidatorStatus(validator: phase0.Validator, currentEpoch: Ep
throw new Error("ValidatorStatus unknown");
}

/**
* Statuses as defined in https://github.com/ethereum/beacon-APIs/pull/614
*/
export type BuilderStatus = "pending" | "active" | "exited";

/**
* Get the status of the builder, `finalizedEpoch` refers to `state.finalized_checkpoint.epoch`
*/
export function getBuilderStatus(builder: gloas.Builder, finalizedEpoch: Epoch): BuilderStatus {
if (builder.withdrawableEpoch !== FAR_FUTURE_EPOCH) {
return "exited";
}
// Same as is_active_builder check with withdrawable epoch already confirmed to be FAR_FUTURE_EPOCH
if (builder.depositEpoch < finalizedEpoch) {
return "active";
}
return "pending";
}

export function mapToGeneralStatus(subStatus: ValidatorStatus): GeneralValidatorStatus {
switch (subStatus) {
case "active_ongoing":
Expand Down
Loading