Skip to content
Draft
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 .wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Golang
Gossipsub
Grafana
Grandine
Gwei
HTTPS
HackMD
Hashicorp
Expand Down Expand Up @@ -169,6 +170,7 @@ flamegraphs
floodsub
fsSL
getNetworkIdentity
gloas
gnosis
gpg
heapdump
Expand Down Expand Up @@ -242,6 +244,7 @@ tcp
testnet
testnets
todo
trustless
typesafe
udp
unpkg
Expand Down
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
Loading
Loading