Skip to content
Draft
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
47 changes: 47 additions & 0 deletions packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ export const LivenessResponseDataType = new ContainerType(
{jsonCase: "eth2"}
);

export const BuilderPreferencesSubmissionType = new ContainerType(
{
/** The BLS public key of the validator expressing these preferences */
validatorPubkey: ssz.BLSPubkey,
/** Preferences and auth to be submitted to the builder identified by `request.auth.message.data` */
request: ssz.gloas.BuilderPreferencesRequestV1,
},
{jsonCase: "eth2"}
);

export const ValidatorIndicesType = ArrayOf(ssz.ValidatorIndex);
export const AttesterDutyListType = ArrayOf(AttesterDutyType);
export const ProposerDutyListType = ArrayOf(ProposerDutyType);
Expand All @@ -246,6 +256,7 @@ export const SignedValidatorRegistrationV1ListType = ArrayOf(
ssz.bellatrix.SignedValidatorRegistrationV1,
VALIDATOR_REGISTRY_LIMIT
);
export const BuilderPreferencesSubmissionListType = ArrayOf(BuilderPreferencesSubmissionType, VALIDATOR_REGISTRY_LIMIT);

export type ValidatorIndices = ValueOf<typeof ValidatorIndicesType>;
export type AttesterDuty = ValueOf<typeof AttesterDutyType>;
Expand Down Expand Up @@ -273,6 +284,8 @@ export type SyncCommitteeSelectionList = ValueOf<typeof SyncCommitteeSelectionLi
export type LivenessResponseData = ValueOf<typeof LivenessResponseDataType>;
export type LivenessResponseDataList = ValueOf<typeof LivenessResponseDataListType>;
export type SignedValidatorRegistrationV1List = ValueOf<typeof SignedValidatorRegistrationV1ListType>;
export type BuilderPreferencesSubmission = ValueOf<typeof BuilderPreferencesSubmissionType>;
export type BuilderPreferencesSubmissionList = ValueOf<typeof BuilderPreferencesSubmissionListType>;

export type Endpoints = {
/**
Expand Down Expand Up @@ -646,6 +659,23 @@ export type Endpoints = {
EmptyResponseData,
EmptyMeta
>;

/**
* Submit per-builder preferences to be forwarded to external builders
*
* The beacon node forwards each request to the builder identified by `request.auth.message.data`
* and caches the auth to authenticate bid requests at proposal time.
*
* Note: this is a Lodestar-specific (non-standardized) endpoint,
* see https://github.com/ethereum/beacon-APIs/issues/600
*/
submitBuilderPreferences: Endpoint<
"POST",
{submissions: BuilderPreferencesSubmissionList},
{body: unknown},
EmptyResponseData,
EmptyMeta
>;
};

export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoints> {
Expand Down Expand Up @@ -1210,6 +1240,23 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
requestWireFormat: WireFormat.ssz,
},
},
submitBuilderPreferences: {
url: "/eth/v1/validator/builder_preferences",
method: "POST",
req: {
writeReqJson: ({submissions}) => ({body: BuilderPreferencesSubmissionListType.toJson(submissions)}),
parseReqJson: ({body}) => ({submissions: BuilderPreferencesSubmissionListType.fromJson(body)}),
writeReqSsz: ({submissions}) => ({body: BuilderPreferencesSubmissionListType.serialize(submissions)}),
parseReqSsz: ({body}) => ({submissions: BuilderPreferencesSubmissionListType.deserialize(body)}),
schema: {
body: Schema.ObjectArray,
},
},
resp: EmptyResponseCodec,
init: {
requestWireFormat: WireFormat.ssz,
},
},
};
}

Expand Down
181 changes: 179 additions & 2 deletions packages/api/src/builder/routes.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import {ChainForkConfig} from "@lodestar/config";
import {ForkName, VALIDATOR_REGISTRY_LIMIT, isForkPostDeneb} from "@lodestar/params";
import {ForkName, ForkPostGloas, VALIDATOR_REGISTRY_LIMIT, isForkPostDeneb} from "@lodestar/params";
import {
ArrayOf,
BLSPubkey,
ExecutionPayload,
ExecutionPayloadAndBlobsBundle,
Root,
SignedBeaconBlock,
SignedBlindedBeaconBlock,
SignedBuilderBid,
Slot,
WithOptionalBytes,
bellatrix,
gloas,
ssz,
} from "@lodestar/types";
import {fromHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
Expand All @@ -23,7 +25,7 @@ import {
EmptyResponseData,
WithVersion,
} from "../utils/codecs.js";
import {getPostBellatrixForkTypes, getPostDenebForkTypes, toForkName} from "../utils/fork.js";
import {getPostBellatrixForkTypes, getPostDenebForkTypes, getPostGloasForkTypes, toForkName} from "../utils/fork.js";
import {fromHeaders} from "../utils/headers.js";
import {Endpoint, RouteDefinitions, Schema} from "../utils/index.js";
import {MetaHeader, VersionCodec, VersionMeta} from "../utils/metadata.js";
Expand All @@ -35,6 +37,9 @@ import {WireFormat} from "../utils/wireFormat.js";
// It is important that this type indicates that there might be no value to ensure it is properly handled downstream.
export type MaybeSignedBuilderBid = SignedBuilderBid | undefined;

// Same as `MaybeSignedBuilderBid`, the builder responds with 204 if no bid is available
export type MaybeSignedExecutionPayloadBid = gloas.SignedExecutionPayloadBid | undefined;

const RegistrationsType = ArrayOf(ssz.bellatrix.SignedValidatorRegistrationV1, VALIDATOR_REGISTRY_LIMIT);

export type Endpoints = {
Expand Down Expand Up @@ -82,6 +87,41 @@ export type Endpoints = {
EmptyResponseData,
EmptyMeta
>;

getExecutionPayloadBid: Endpoint<
"POST",
{
slot: Slot;
parentHash: Root;
parentRoot: Root;
proposerPubkey: BLSPubkey;
/** Optional auth to allow the builder to verify that the request was sent by the proposer */
requestAuth?: gloas.SignedRequestAuthV1;
},
{
params: {slot: Slot; parent_hash: string; parent_root: string; proposer_pubkey: string};
body: unknown;
headers: {[MetaHeader.Version]: string};
},
MaybeSignedExecutionPayloadBid,
VersionMeta
>;

submitSignedBeaconBlock: Endpoint<
"POST",
{signedBlock: WithOptionalBytes<SignedBeaconBlock<ForkPostGloas>>},
{body: unknown; headers: {[MetaHeader.Version]: string}},
EmptyResponseData,
EmptyMeta
>;

submitBuilderPreferences: Endpoint<
"POST",
{validatorPubkey: BLSPubkey; request: gloas.BuilderPreferencesRequestV1},
{params: {validator_pubkey: string}; body: unknown; headers: {[MetaHeader.Version]: string}},
EmptyResponseData,
EmptyMeta
>;
};

export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoints> {
Expand Down Expand Up @@ -223,5 +263,142 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
},
resp: EmptyResponseCodec,
},
getExecutionPayloadBid: {
url: "/eth/v1/builder/execution_payload_bid/{slot}/{parent_hash}/{parent_root}/{proposer_pubkey}",
method: "POST",
req: {
writeReqJson: ({slot, parentHash, parentRoot, proposerPubkey, requestAuth}) => ({
params: {
slot,
parent_hash: toRootHex(parentHash),
parent_root: toRootHex(parentRoot),
proposer_pubkey: toPubkeyHex(proposerPubkey),
},
body: requestAuth && ssz.gloas.SignedRequestAuthV1.toJson(requestAuth),
headers: {[MetaHeader.Version]: config.getForkName(slot)},
}),
parseReqJson: ({params, body}) => ({
slot: params.slot,
parentHash: fromHex(params.parent_hash),
parentRoot: fromHex(params.parent_root),
proposerPubkey: fromHex(params.proposer_pubkey),
requestAuth: body != null ? ssz.gloas.SignedRequestAuthV1.fromJson(body) : undefined,
}),
writeReqSsz: ({slot, parentHash, parentRoot, proposerPubkey, requestAuth}) => ({
params: {
slot,
parent_hash: toRootHex(parentHash),
parent_root: toRootHex(parentRoot),
proposer_pubkey: toPubkeyHex(proposerPubkey),
},
// If there is no request auth, the request will be sent without a body
body: (requestAuth && ssz.gloas.SignedRequestAuthV1.serialize(requestAuth)) as Uint8Array,
headers: {[MetaHeader.Version]: config.getForkName(slot)},
}),
parseReqSsz: ({params, body}) => ({
slot: params.slot,
parentHash: fromHex(params.parent_hash),
parentRoot: fromHex(params.parent_root),
proposerPubkey: fromHex(params.proposer_pubkey),
requestAuth: ssz.gloas.SignedRequestAuthV1.deserialize(body),
}),
schema: {
params: {
slot: Schema.UintRequired,
parent_hash: Schema.StringRequired,
parent_root: Schema.StringRequired,
proposer_pubkey: Schema.StringRequired,
},
body: Schema.Object,
headers: {[MetaHeader.Version]: Schema.String},
},
},
resp: {
data: WithVersion<MaybeSignedExecutionPayloadBid, VersionMeta>(
(fork: ForkName) => getPostGloasForkTypes(fork).SignedExecutionPayloadBid
),
meta: VersionCodec,
},
init: {
requestWireFormat: WireFormat.ssz,
},
},
submitSignedBeaconBlock: {
url: "/eth/v1/builder/beacon_blocks",
method: "POST",
req: {
writeReqJson: ({signedBlock}) => {
const fork = config.getForkName(signedBlock.data.message.slot);
return {
body: getPostGloasForkTypes(fork).SignedBeaconBlock.toJson(signedBlock.data),
headers: {
[MetaHeader.Version]: fork,
},
};
},
parseReqJson: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedBlock: {data: getPostGloasForkTypes(fork).SignedBeaconBlock.fromJson(body)},
};
},
writeReqSsz: ({signedBlock}) => {
const fork = config.getForkName(signedBlock.data.message.slot);
return {
body: signedBlock.bytes ?? getPostGloasForkTypes(fork).SignedBeaconBlock.serialize(signedBlock.data),
headers: {
[MetaHeader.Version]: fork,
},
};
},
parseReqSsz: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedBlock: {data: getPostGloasForkTypes(fork).SignedBeaconBlock.deserialize(body)},
};
},
schema: {
body: Schema.Object,
headers: {[MetaHeader.Version]: Schema.String},
},
},
resp: EmptyResponseCodec,
init: {
requestWireFormat: WireFormat.ssz,
},
},
submitBuilderPreferences: {
url: "/eth/v1/builder/builder_preferences/{validator_pubkey}",
method: "POST",
req: {
writeReqJson: ({validatorPubkey, request}) => ({
params: {validator_pubkey: toPubkeyHex(validatorPubkey)},
body: ssz.gloas.BuilderPreferencesRequestV1.toJson(request),
headers: {[MetaHeader.Version]: config.getForkName(request.auth.message.slot)},
}),
parseReqJson: ({params, body}) => ({
validatorPubkey: fromHex(params.validator_pubkey),
request: ssz.gloas.BuilderPreferencesRequestV1.fromJson(body),
}),
writeReqSsz: ({validatorPubkey, request}) => ({
params: {validator_pubkey: toPubkeyHex(validatorPubkey)},
body: ssz.gloas.BuilderPreferencesRequestV1.serialize(request),
headers: {[MetaHeader.Version]: config.getForkName(request.auth.message.slot)},
}),
parseReqSsz: ({params, body}) => ({
validatorPubkey: fromHex(params.validator_pubkey),
request: ssz.gloas.BuilderPreferencesRequestV1.deserialize(body),
}),
schema: {
params: {validator_pubkey: Schema.StringRequired},
body: Schema.Object,
headers: {[MetaHeader.Version]: Schema.String},
},
},
resp: EmptyResponseCodec,
init: {
requestWireFormat: WireFormat.ssz,
},
},
};
}
11 changes: 11 additions & 0 deletions packages/api/test/unit/beacon/testData/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,15 @@ export const testData: GenericServerTestCases<Endpoints> = {
args: {registrations: [ssz.bellatrix.SignedValidatorRegistrationV1.defaultValue()]},
res: undefined,
},
submitBuilderPreferences: {
args: {
submissions: [
{
validatorPubkey: new Uint8Array(48).fill(1),
request: ssz.gloas.BuilderPreferencesRequestV1.defaultValue(),
},
],
},
res: undefined,
},
};
2 changes: 1 addition & 1 deletion packages/api/test/unit/builder/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {testData} from "./testData.js";

describe("builder", () => {
runGenericServerTest<Endpoints>(
createChainForkConfig({...defaultChainConfig, ELECTRA_FORK_EPOCH: 0}),
createChainForkConfig({...defaultChainConfig, ELECTRA_FORK_EPOCH: 0, GLOAS_FORK_EPOCH: 1}),
getClient,
getRoutes,
testData
Expand Down
32 changes: 31 additions & 1 deletion packages/api/test/unit/builder/testData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {fromHexString} from "@chainsafe/ssz";
import {ForkName} from "@lodestar/params";
import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params";
import {ssz} from "@lodestar/types";
import {Endpoints} from "../../../src/builder/routes.js";
import {GenericServerTestCases} from "../../utils/genericServerTest.js";
Expand All @@ -8,6 +8,18 @@ import {GenericServerTestCases} from "../../utils/genericServerTest.js";
const pubkeyRand = "0x84105a985058fc8740a48bf1ede9d223ef09e8c6b1735ba0a55cf4a9ff2ff92376b778798365e488dab07a652eb04576";
const root = new Uint8Array(32).fill(1);

// GLOAS_FORK_EPOCH is set to 1 in test config
const gloasSlot = SLOTS_PER_EPOCH;

const requestAuth = ssz.gloas.SignedRequestAuthV1.defaultValue();
requestAuth.message.slot = gloasSlot;

const signedBlock = ssz.gloas.SignedBeaconBlock.defaultValue();
signedBlock.message.slot = gloasSlot;

const builderPreferencesRequest = ssz.gloas.BuilderPreferencesRequestV1.defaultValue();
builderPreferencesRequest.auth.message.slot = gloasSlot;

export const testData: GenericServerTestCases<Endpoints> = {
status: {
args: undefined,
Expand All @@ -29,4 +41,22 @@ export const testData: GenericServerTestCases<Endpoints> = {
args: {signedBlindedBlock: {data: ssz.fulu.SignedBlindedBeaconBlock.defaultValue()}},
res: undefined,
},
getExecutionPayloadBid: {
args: {
slot: gloasSlot,
parentHash: root,
parentRoot: root,
proposerPubkey: fromHexString(pubkeyRand),
requestAuth,
},
res: {data: ssz.gloas.SignedExecutionPayloadBid.defaultValue(), meta: {version: ForkName.gloas}},
},
submitSignedBeaconBlock: {
args: {signedBlock: {data: signedBlock}},
res: undefined,
},
submitBuilderPreferences: {
args: {validatorPubkey: fromHexString(pubkeyRand), request: builderPreferencesRequest},
res: undefined,
},
};
Loading
Loading