Skip to content
Merged
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
3 changes: 3 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,9 @@ export type {BlockHeaderResponse, BlockId} from "./block.js";
export {BroadcastValidation} from "./block.js";
// TODO: Review if re-exporting all these types is necessary
export type {
BuilderId,
BuilderResponse,
BuilderStatus,
EpochCommitteeResponse,
EpochSyncCommitteeResponse,
FinalityCheckpoints,
Expand Down
65 changes: 63 additions & 2 deletions 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 All @@ -22,7 +23,7 @@ import {
ExecutionOptimisticFinalizedAndVersionCodec,
ExecutionOptimisticFinalizedAndVersionMeta,
} from "../../../utils/metadata.js";
import {fromValidatorIdsStr, toValidatorIdsStr} from "../../../utils/serdes.js";
import {fromBuilderIdsStr, fromValidatorIdsStr, toBuilderIdsStr, toValidatorIdsStr} from "../../../utils/serdes.js";
import {WireFormat} from "../../../utils/wireFormat.js";
import {RootResponse, RootResponseType} from "./block.js";

Expand All @@ -37,8 +38,9 @@ export type StateArgs = {
};

export type ValidatorId = string | number;
Comment thread
nflaig marked this conversation as resolved.
export type BuilderId = string | number;

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

export const RandaoResponseType = new ContainerType({
randao: ssz.Root,
Expand All @@ -57,6 +59,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 +91,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 +214,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?: BuilderId[];
statuses?: BuilderStatus[];
},
{params: {state_id: string}; body: {ids?: string[]; statuses?: BuilderStatus[]}},
BuilderResponseList,
ExecutionOptimisticAndFinalizedMeta
>;

/**
* Get validator identities from state
*
Expand Down Expand Up @@ -489,6 +523,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: toBuilderIdsStr(builderIds),
statuses,
},
}),
parseReqJson: ({params, body = {}}) => ({
stateId: params.state_id,
builderIds: fromBuilderIdsStr(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
3 changes: 3 additions & 0 deletions packages/api/src/utils/serdes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export function fromValidatorIdsStr(ids?: string[]): (string | number)[] | undef
return ids?.map((id) => (typeof id === "string" && id.startsWith("0x") ? id : fromU64Str(id)));
}

export const toBuilderIdsStr = toValidatorIdsStr;
export const fromBuilderIdsStr = fromValidatorIdsStr;

const GRAFFITI_HEX_LENGTH = 66;

export function toGraffitiHex(utf8?: string): string | undefined {
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(

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.

this could also be made reusable between validators/builders but I don't like to be overly DRY for the sake of it, and it would definitely become less readable, we also don't wanna couple the code too much as there might be a drift in the future between builders and validators

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.

Agreed. I would keep the builder and validator paths separate here for readability and to avoid coupling them before we know whether the APIs drift. No code change from me on this thread.

id: routes.beacon.BuilderId | 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 {

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.

we might need a new binding for that cc @spiral-ladder

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.

Good point. This PR already adds the TypeScript state-view surface and the native wrapper call for getBuildersLength, and the current native portability check is passing on this head. If the underlying native binding implementation needs an explicit addition, I would leave that to the native binding follow-up/owner rather than adding more JS-side churn here.

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
Loading
Loading