@lodestar/api 1.47.0-dev.f591cb177e → 1.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/lib/beacon/routes/beacon/block.d.ts +7 -0
  2. package/lib/beacon/routes/beacon/block.d.ts.map +1 -1
  3. package/lib/beacon/routes/beacon/block.js +7 -3
  4. package/lib/beacon/routes/beacon/block.js.map +1 -1
  5. package/lib/beacon/routes/debug.d.ts +3 -3
  6. package/lib/beacon/routes/debug.js +3 -3
  7. package/lib/beacon/routes/debug.js.map +1 -1
  8. package/lib/beacon/routes/validator.d.ts +92 -5
  9. package/lib/beacon/routes/validator.d.ts.map +1 -1
  10. package/lib/beacon/routes/validator.js +121 -16
  11. package/lib/beacon/routes/validator.js.map +1 -1
  12. package/lib/beacon/server/events.d.ts.map +1 -1
  13. package/lib/beacon/server/events.js +10 -2
  14. package/lib/beacon/server/events.js.map +1 -1
  15. package/lib/builder/routes.d.ts +48 -1
  16. package/lib/builder/routes.d.ts.map +1 -1
  17. package/lib/builder/routes.js +173 -1
  18. package/lib/builder/routes.js.map +1 -1
  19. package/lib/keymanager/index.d.ts +2 -2
  20. package/lib/keymanager/index.d.ts.map +1 -1
  21. package/lib/keymanager/index.js +1 -1
  22. package/lib/keymanager/index.js.map +1 -1
  23. package/lib/keymanager/routes.d.ts +63 -0
  24. package/lib/keymanager/routes.d.ts.map +1 -1
  25. package/lib/keymanager/routes.js +147 -0
  26. package/lib/keymanager/routes.js.map +1 -1
  27. package/lib/utils/client/response.js +1 -1
  28. package/lib/utils/client/response.js.map +1 -1
  29. package/lib/utils/metadata.d.ts +3 -0
  30. package/lib/utils/metadata.d.ts.map +1 -1
  31. package/lib/utils/metadata.js +4 -0
  32. package/lib/utils/metadata.js.map +1 -1
  33. package/lib/utils/types.d.ts +1 -1
  34. package/lib/utils/types.d.ts.map +1 -1
  35. package/package.json +8 -8
  36. package/src/beacon/routes/beacon/block.ts +18 -4
  37. package/src/beacon/routes/debug.ts +3 -3
  38. package/src/beacon/routes/validator.ts +178 -20
  39. package/src/beacon/server/events.ts +10 -3
  40. package/src/builder/routes.ts +227 -2
  41. package/src/keymanager/index.ts +9 -1
  42. package/src/keymanager/routes.ts +227 -0
  43. package/src/utils/client/response.ts +1 -1
  44. package/src/utils/metadata.ts +5 -0
  45. package/src/utils/types.ts +1 -1
@@ -1,16 +1,18 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {ForkName, VALIDATOR_REGISTRY_LIMIT, isForkPostDeneb} from "@lodestar/params";
2
+ import {ForkName, ForkPostGloas, VALIDATOR_REGISTRY_LIMIT, isForkPostDeneb} from "@lodestar/params";
3
3
  import {
4
4
  ArrayOf,
5
5
  BLSPubkey,
6
6
  ExecutionPayload,
7
7
  ExecutionPayloadAndBlobsBundle,
8
8
  Root,
9
+ SignedBeaconBlock,
9
10
  SignedBlindedBeaconBlock,
10
11
  SignedBuilderBid,
11
12
  Slot,
12
13
  WithOptionalBytes,
13
14
  bellatrix,
15
+ gloas,
14
16
  ssz,
15
17
  } from "@lodestar/types";
16
18
  import {fromHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
@@ -23,7 +25,7 @@ import {
23
25
  EmptyResponseData,
24
26
  WithVersion,
25
27
  } from "../utils/codecs.js";
26
- import {getPostBellatrixForkTypes, getPostDenebForkTypes, toForkName} from "../utils/fork.js";
28
+ import {getPostBellatrixForkTypes, getPostDenebForkTypes, getPostGloasForkTypes, toForkName} from "../utils/fork.js";
27
29
  import {fromHeaders} from "../utils/headers.js";
28
30
  import {Endpoint, RouteDefinitions, Schema} from "../utils/index.js";
29
31
  import {MetaHeader, VersionCodec, VersionMeta} from "../utils/metadata.js";
@@ -35,6 +37,9 @@ import {WireFormat} from "../utils/wireFormat.js";
35
37
  // It is important that this type indicates that there might be no value to ensure it is properly handled downstream.
36
38
  export type MaybeSignedBuilderBid = SignedBuilderBid | undefined;
37
39
 
40
+ // Same as `MaybeSignedBuilderBid`, the builder responds with 204 if no bid is available
41
+ export type MaybeSignedExecutionPayloadBid = gloas.SignedExecutionPayloadBid | undefined;
42
+
38
43
  const RegistrationsType = ArrayOf(ssz.bellatrix.SignedValidatorRegistrationV1, VALIDATOR_REGISTRY_LIMIT);
39
44
 
40
45
  export type Endpoints = {
@@ -82,6 +87,45 @@ export type Endpoints = {
82
87
  EmptyResponseData,
83
88
  EmptyMeta
84
89
  >;
90
+
91
+ getExecutionPayloadBid: Endpoint<
92
+ "POST",
93
+ {
94
+ slot: Slot;
95
+ parentHash: Root;
96
+ parentRoot: Root;
97
+ proposerPubkey: BLSPubkey;
98
+ /** Authenticates the requesting proposer to the builder */
99
+ requestAuth: gloas.SignedBuilderRequestAuth;
100
+ /** Unix timestamp in milliseconds at which the request was sent */
101
+ dateMilliseconds: number;
102
+ /** The proposer's timeout for the request in milliseconds, measured from `dateMilliseconds` */
103
+ timeoutMs: number;
104
+ },
105
+ {
106
+ params: {slot: Slot; parent_hash: string; parent_root: string; proposer_pubkey: string};
107
+ body: unknown;
108
+ headers: {[MetaHeader.Version]: string; [MetaHeader.DateMilliseconds]: string; [MetaHeader.TimeoutMs]: string};
109
+ },
110
+ MaybeSignedExecutionPayloadBid,
111
+ VersionMeta
112
+ >;
113
+
114
+ submitSignedBeaconBlock: Endpoint<
115
+ "POST",
116
+ {signedBlock: WithOptionalBytes<SignedBeaconBlock<ForkPostGloas>>},
117
+ {body: unknown; headers: {[MetaHeader.Version]: string}},
118
+ EmptyResponseData,
119
+ EmptyMeta
120
+ >;
121
+
122
+ submitBuilderPreferences: Endpoint<
123
+ "POST",
124
+ {proposerPubkey: BLSPubkey; request: gloas.BuilderPreferencesRequest},
125
+ {params: {proposer_pubkey: string}; body: unknown; headers: {[MetaHeader.Version]: string}},
126
+ EmptyResponseData,
127
+ EmptyMeta
128
+ >;
85
129
  };
86
130
 
87
131
  export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoints> {
@@ -223,5 +267,186 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
223
267
  },
224
268
  resp: EmptyResponseCodec,
225
269
  },
270
+ getExecutionPayloadBid: {
271
+ url: "/eth/v1/builder/execution_payload_bid/{slot}/{parent_hash}/{parent_root}/{proposer_pubkey}",
272
+ method: "POST",
273
+ req: {
274
+ writeReqJson: ({slot, parentHash, parentRoot, proposerPubkey, requestAuth, dateMilliseconds, timeoutMs}) => ({
275
+ params: {
276
+ slot,
277
+ parent_hash: toRootHex(parentHash),
278
+ parent_root: toRootHex(parentRoot),
279
+ proposer_pubkey: toPubkeyHex(proposerPubkey),
280
+ },
281
+ body: ssz.gloas.SignedBuilderRequestAuth.toJson(requestAuth),
282
+ headers: {
283
+ [MetaHeader.Version]: config.getForkName(slot),
284
+ [MetaHeader.DateMilliseconds]: dateMilliseconds.toString(),
285
+ [MetaHeader.TimeoutMs]: timeoutMs.toString(),
286
+ },
287
+ }),
288
+ parseReqJson: ({params, body, headers}) => {
289
+ toForkName(fromHeaders(headers, MetaHeader.Version));
290
+ return {
291
+ slot: params.slot,
292
+ parentHash: fromHex(params.parent_hash),
293
+ parentRoot: fromHex(params.parent_root),
294
+ proposerPubkey: fromHex(params.proposer_pubkey),
295
+ requestAuth: ssz.gloas.SignedBuilderRequestAuth.fromJson(body),
296
+ dateMilliseconds: parseRequiredUintHeader(
297
+ fromHeaders(headers, MetaHeader.DateMilliseconds),
298
+ MetaHeader.DateMilliseconds
299
+ ),
300
+ timeoutMs: parseRequiredUintHeader(fromHeaders(headers, MetaHeader.TimeoutMs), MetaHeader.TimeoutMs),
301
+ };
302
+ },
303
+ writeReqSsz: ({slot, parentHash, parentRoot, proposerPubkey, requestAuth, dateMilliseconds, timeoutMs}) => ({
304
+ params: {
305
+ slot,
306
+ parent_hash: toRootHex(parentHash),
307
+ parent_root: toRootHex(parentRoot),
308
+ proposer_pubkey: toPubkeyHex(proposerPubkey),
309
+ },
310
+ body: ssz.gloas.SignedBuilderRequestAuth.serialize(requestAuth),
311
+ headers: {
312
+ [MetaHeader.Version]: config.getForkName(slot),
313
+ [MetaHeader.DateMilliseconds]: dateMilliseconds.toString(),
314
+ [MetaHeader.TimeoutMs]: timeoutMs.toString(),
315
+ },
316
+ }),
317
+ parseReqSsz: ({params, body, headers}) => {
318
+ toForkName(fromHeaders(headers, MetaHeader.Version));
319
+ return {
320
+ slot: params.slot,
321
+ parentHash: fromHex(params.parent_hash),
322
+ parentRoot: fromHex(params.parent_root),
323
+ proposerPubkey: fromHex(params.proposer_pubkey),
324
+ requestAuth: ssz.gloas.SignedBuilderRequestAuth.deserialize(body),
325
+ dateMilliseconds: parseRequiredUintHeader(
326
+ fromHeaders(headers, MetaHeader.DateMilliseconds),
327
+ MetaHeader.DateMilliseconds
328
+ ),
329
+ timeoutMs: parseRequiredUintHeader(fromHeaders(headers, MetaHeader.TimeoutMs), MetaHeader.TimeoutMs),
330
+ };
331
+ },
332
+ schema: {
333
+ params: {
334
+ slot: Schema.UintRequired,
335
+ parent_hash: Schema.StringRequired,
336
+ parent_root: Schema.StringRequired,
337
+ proposer_pubkey: Schema.StringRequired,
338
+ },
339
+ body: Schema.Object,
340
+ headers: {
341
+ [MetaHeader.Version]: Schema.String,
342
+ [MetaHeader.DateMilliseconds]: Schema.String,
343
+ [MetaHeader.TimeoutMs]: Schema.String,
344
+ },
345
+ },
346
+ },
347
+ resp: {
348
+ data: WithVersion<MaybeSignedExecutionPayloadBid, VersionMeta>(
349
+ (fork: ForkName) => getPostGloasForkTypes(fork).SignedExecutionPayloadBid
350
+ ),
351
+ meta: VersionCodec,
352
+ },
353
+ init: {
354
+ requestWireFormat: WireFormat.ssz,
355
+ },
356
+ },
357
+ submitSignedBeaconBlock: {
358
+ url: "/eth/v1/builder/beacon_blocks",
359
+ method: "POST",
360
+ req: {
361
+ writeReqJson: ({signedBlock}) => {
362
+ const fork = config.getForkName(signedBlock.data.message.slot);
363
+ return {
364
+ body: getPostGloasForkTypes(fork).SignedBeaconBlock.toJson(signedBlock.data),
365
+ headers: {
366
+ [MetaHeader.Version]: fork,
367
+ },
368
+ };
369
+ },
370
+ parseReqJson: ({body, headers}) => {
371
+ const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
372
+ return {
373
+ signedBlock: {data: getPostGloasForkTypes(fork).SignedBeaconBlock.fromJson(body)},
374
+ };
375
+ },
376
+ writeReqSsz: ({signedBlock}) => {
377
+ const fork = config.getForkName(signedBlock.data.message.slot);
378
+ return {
379
+ body: signedBlock.bytes ?? getPostGloasForkTypes(fork).SignedBeaconBlock.serialize(signedBlock.data),
380
+ headers: {
381
+ [MetaHeader.Version]: fork,
382
+ },
383
+ };
384
+ },
385
+ parseReqSsz: ({body, headers}) => {
386
+ const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
387
+ return {
388
+ signedBlock: {data: getPostGloasForkTypes(fork).SignedBeaconBlock.deserialize(body)},
389
+ };
390
+ },
391
+ schema: {
392
+ body: Schema.Object,
393
+ headers: {[MetaHeader.Version]: Schema.String},
394
+ },
395
+ },
396
+ resp: EmptyResponseCodec,
397
+ init: {
398
+ requestWireFormat: WireFormat.ssz,
399
+ },
400
+ },
401
+ submitBuilderPreferences: {
402
+ url: "/eth/v1/builder/builder_preferences/{proposer_pubkey}",
403
+ method: "POST",
404
+ req: {
405
+ writeReqJson: ({proposerPubkey, request}) => ({
406
+ params: {proposer_pubkey: toPubkeyHex(proposerPubkey)},
407
+ body: ssz.gloas.BuilderPreferencesRequest.toJson(request),
408
+ headers: {[MetaHeader.Version]: config.getForkName(request.auth.message.slot)},
409
+ }),
410
+ parseReqJson: ({params, body, headers}) => {
411
+ toForkName(fromHeaders(headers, MetaHeader.Version));
412
+ return {
413
+ proposerPubkey: fromHex(params.proposer_pubkey),
414
+ request: ssz.gloas.BuilderPreferencesRequest.fromJson(body),
415
+ };
416
+ },
417
+ writeReqSsz: ({proposerPubkey, request}) => ({
418
+ params: {proposer_pubkey: toPubkeyHex(proposerPubkey)},
419
+ body: ssz.gloas.BuilderPreferencesRequest.serialize(request),
420
+ headers: {[MetaHeader.Version]: config.getForkName(request.auth.message.slot)},
421
+ }),
422
+ parseReqSsz: ({params, body, headers}) => {
423
+ toForkName(fromHeaders(headers, MetaHeader.Version));
424
+ return {
425
+ proposerPubkey: fromHex(params.proposer_pubkey),
426
+ request: ssz.gloas.BuilderPreferencesRequest.deserialize(body),
427
+ };
428
+ },
429
+ schema: {
430
+ params: {proposer_pubkey: Schema.StringRequired},
431
+ body: Schema.Object,
432
+ headers: {[MetaHeader.Version]: Schema.String},
433
+ },
434
+ },
435
+ resp: EmptyResponseCodec,
436
+ init: {
437
+ requestWireFormat: WireFormat.ssz,
438
+ },
439
+ },
226
440
  };
227
441
  }
442
+
443
+ function parseRequiredUintHeader(value: string, header: MetaHeader): number {
444
+ if (!/^\d+$/.test(value)) {
445
+ throw Error(`${header} must be a non-negative integer`);
446
+ }
447
+ const parsed = Number(value);
448
+ if (!Number.isSafeInteger(parsed)) {
449
+ throw Error(`${header} must be a safe integer`);
450
+ }
451
+ return parsed;
452
+ }
@@ -7,6 +7,8 @@ import * as keymanager from "./client.js";
7
7
 
8
8
  export type {
9
9
  BuilderBoostFactorData,
10
+ BuilderConfigData,
11
+ BuilderEntryConfig,
10
12
  Endpoints,
11
13
  FeeRecipientData,
12
14
  GasLimitData,
@@ -19,7 +21,13 @@ export type {
19
21
  SignerDefinition,
20
22
  SlashingProtectionData,
21
23
  } from "./routes.js";
22
- export {DeleteRemoteKeyStatus, DeletionStatus, ImportRemoteKeyStatus, ImportStatus} from "./routes.js";
24
+ export {
25
+ DeleteRemoteKeyStatus,
26
+ DeletionStatus,
27
+ ImportRemoteKeyStatus,
28
+ ImportStatus,
29
+ builderConfigDataFromJson,
30
+ } from "./routes.js";
23
31
 
24
32
  export type {ApiClient};
25
33
 
@@ -1,6 +1,13 @@
1
1
  import {ContainerType, ValueOf} from "@chainsafe/ssz";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
+ import {
4
+ MAX_BUILDER_AUTH_DATA_SIZE,
5
+ MAX_BUILDER_ENTRIES,
6
+ MAX_BUILDER_PUBKEYS,
7
+ MAX_BUILDER_URL_SIZE,
8
+ } from "@lodestar/params";
3
9
  import {Epoch, phase0, ssz, stringType} from "@lodestar/types";
10
+ import {isValidAsciiHttpUrl} from "@lodestar/utils";
4
11
  import {
5
12
  EmptyArgs,
6
13
  EmptyMeta,
@@ -96,6 +103,137 @@ export type GraffitiData = ValueOf<typeof GraffitiDataType>;
96
103
  export type GasLimitData = ValueOf<typeof GasLimitDataType>;
97
104
  export type BuilderBoostFactorData = ValueOf<typeof BuilderBoostFactorDataType>;
98
105
 
106
+ /** Configuration for a single builder to request bids from */
107
+ export type BuilderEntryConfig = {
108
+ /** URL the bid requests for this entry are sent to */
109
+ url: string;
110
+ /** Auth data hex string as agreed with the builder, derived from the UTF-8 bytes of the builder url when omitted */
111
+ authData?: string;
112
+ /** Builder BLS pubkeys this entry accepts bids from, empty or omitted accepts any builder */
113
+ builderPubkeys?: string[];
114
+ /** Cap in Gwei on the execution payment counted from this builder's bids */
115
+ maxExecutionPayment?: bigint;
116
+ /** Floor in Gwei on the counted total payment accepted from this builder's bids */
117
+ minBid?: bigint;
118
+ /** Percentage multiplier weighting this builder's bids during selection */
119
+ builderBoostFactor?: bigint;
120
+ };
121
+
122
+ /** Per-key builder configuration for requesting and selecting bids */
123
+ export type BuilderConfigData = {
124
+ /** Default for entries that do not set their own `minBid`, also applies to p2p bids */
125
+ minBid?: bigint;
126
+ /** Default for entries that do not set their own `builderBoostFactor`, also applies to p2p bids */
127
+ builderBoostFactor?: bigint;
128
+ /** Omitted means use the validator client's builders, empty means request bids from none */
129
+ builders?: BuilderEntryConfig[];
130
+ };
131
+
132
+ const BUILDER_AUTH_DATA_PATTERN = new RegExp(`^0x(?:[a-fA-F0-9]{2}){1,${MAX_BUILDER_AUTH_DATA_SIZE}}$`);
133
+ const PUBKEY_PATTERN = /^0x[a-fA-F0-9]{96}$/;
134
+ const UINT64_MAX = 2n ** 64n - 1n;
135
+
136
+ function parseGweiAmount(value: unknown, field: string): bigint | undefined {
137
+ if (value === undefined) return undefined;
138
+ if (typeof value !== "string" || !/^(0|[1-9][0-9]{0,19})$/.test(value)) {
139
+ throw Error(`${field} must be an unsigned 64-bit integer encoded as a decimal string`);
140
+ }
141
+ const parsed = BigInt(value);
142
+ if (parsed > UINT64_MAX) {
143
+ throw Error(`${field} must not exceed 2**64 - 1`);
144
+ }
145
+ return parsed;
146
+ }
147
+
148
+ export function builderConfigDataToJson(config: BuilderConfigData): Record<string, unknown> {
149
+ return {
150
+ ...(config.minBid !== undefined ? {min_bid: config.minBid.toString()} : {}),
151
+ ...(config.builderBoostFactor !== undefined ? {builder_boost_factor: config.builderBoostFactor.toString()} : {}),
152
+ ...(config.builders !== undefined
153
+ ? {
154
+ builders: config.builders.map((entry) => ({
155
+ url: entry.url,
156
+ ...(entry.authData !== undefined ? {auth_data: entry.authData} : {}),
157
+ ...(entry.builderPubkeys !== undefined ? {builder_pubkeys: entry.builderPubkeys} : {}),
158
+ ...(entry.maxExecutionPayment !== undefined
159
+ ? {max_execution_payment: entry.maxExecutionPayment.toString()}
160
+ : {}),
161
+ ...(entry.minBid !== undefined ? {min_bid: entry.minBid.toString()} : {}),
162
+ ...(entry.builderBoostFactor !== undefined
163
+ ? {builder_boost_factor: entry.builderBoostFactor.toString()}
164
+ : {}),
165
+ })),
166
+ }
167
+ : {}),
168
+ };
169
+ }
170
+
171
+ export function builderConfigDataFromJson(json: unknown): BuilderConfigData {
172
+ if (typeof json !== "object" || json === null || Array.isArray(json)) {
173
+ throw Error("Builder config must be an object");
174
+ }
175
+ const {min_bid, builder_boost_factor, builders} = json as Record<string, unknown>;
176
+
177
+ let parsedBuilders: BuilderEntryConfig[] | undefined;
178
+ if (builders !== undefined) {
179
+ if (!Array.isArray(builders)) {
180
+ throw Error("builders must be an array");
181
+ }
182
+ if (builders.length > MAX_BUILDER_ENTRIES) {
183
+ throw Error(`builders must not contain more than ${MAX_BUILDER_ENTRIES} entries, got ${builders.length}`);
184
+ }
185
+ parsedBuilders = builders.map((entry, i): BuilderEntryConfig => {
186
+ if (typeof entry !== "object" || entry === null) {
187
+ throw Error(`builders[${i}] must be an object`);
188
+ }
189
+ const {url, auth_data, builder_pubkeys, max_execution_payment} = entry as Record<string, unknown>;
190
+ if (typeof url !== "string" || url.length === 0) {
191
+ throw Error(`builders[${i}].url must be a non-empty string`);
192
+ }
193
+ if (new TextEncoder().encode(url).length > MAX_BUILDER_URL_SIZE) {
194
+ throw Error(`builders[${i}].url must not exceed ${MAX_BUILDER_URL_SIZE} bytes`);
195
+ }
196
+ if (!isValidAsciiHttpUrl(url)) {
197
+ throw Error(`builders[${i}].url must be a valid HTTP or HTTPS URL using only ASCII characters`);
198
+ }
199
+ if (auth_data !== undefined && (typeof auth_data !== "string" || !BUILDER_AUTH_DATA_PATTERN.test(auth_data))) {
200
+ throw Error(
201
+ `builders[${i}].auth_data must be a non-empty hex string of at most ${MAX_BUILDER_AUTH_DATA_SIZE} bytes`
202
+ );
203
+ }
204
+ let builderPubkeys: string[] | undefined;
205
+ if (builder_pubkeys !== undefined) {
206
+ if (!Array.isArray(builder_pubkeys) || builder_pubkeys.length > MAX_BUILDER_PUBKEYS) {
207
+ throw Error(`builders[${i}].builder_pubkeys must be an array of at most ${MAX_BUILDER_PUBKEYS} pubkeys`);
208
+ }
209
+ builderPubkeys = builder_pubkeys.map((pubkey) => {
210
+ if (typeof pubkey !== "string" || !PUBKEY_PATTERN.test(pubkey)) {
211
+ throw Error(`builders[${i}].builder_pubkeys must contain 48-byte hex pubkeys`);
212
+ }
213
+ return pubkey;
214
+ });
215
+ }
216
+ return {
217
+ url,
218
+ authData: auth_data as string | undefined,
219
+ builderPubkeys,
220
+ maxExecutionPayment: parseGweiAmount(max_execution_payment, `builders[${i}].max_execution_payment`),
221
+ minBid: parseGweiAmount((entry as Record<string, unknown>).min_bid, `builders[${i}].min_bid`),
222
+ builderBoostFactor: parseGweiAmount(
223
+ (entry as Record<string, unknown>).builder_boost_factor,
224
+ `builders[${i}].builder_boost_factor`
225
+ ),
226
+ };
227
+ });
228
+ }
229
+
230
+ return {
231
+ minBid: parseGweiAmount(min_bid, "min_bid"),
232
+ builderBoostFactor: parseGweiAmount(builder_boost_factor, "builder_boost_factor"),
233
+ builders: parsedBuilders,
234
+ };
235
+ }
236
+
99
237
  export type SignerDefinition = {
100
238
  pubkey: PubkeyHex;
101
239
  /**
@@ -117,9 +255,21 @@ export type ProposerConfigResponse = {
117
255
  gasLimit?: number;
118
256
  selection?: string;
119
257
  boostFactor?: string;
258
+ minBid?: string;
259
+ maxExecutionPayment?: string;
260
+ builders?: BuilderEntryConfigResponse[];
120
261
  };
121
262
  };
122
263
 
264
+ export type BuilderEntryConfigResponse = {
265
+ url: string;
266
+ authData?: string;
267
+ builderPubkeys?: string[];
268
+ maxExecutionPayment?: string;
269
+ minBid?: string;
270
+ builderBoostFactor?: string;
271
+ };
272
+
123
273
  /**
124
274
  * JSON serialized representation of a single keystore in EIP-2335: BLS12-381 Keystore format.
125
275
  * ```
@@ -367,6 +517,33 @@ export type Endpoints = {
367
517
  EmptyMeta
368
518
  >;
369
519
 
520
+ /** Get the builder configuration in effect for a validator public key, with omitted values resolved */
521
+ getBuilderConfig: Endpoint<
522
+ // ⏎
523
+ "GET",
524
+ {pubkey: PubkeyHex},
525
+ {params: {pubkey: string}},
526
+ BuilderConfigData,
527
+ EmptyMeta
528
+ >;
529
+ /** Set the builder configuration for a validator public key, replacing any stored configuration in full */
530
+ setBuilderConfig: Endpoint<
531
+ "POST",
532
+ {pubkey: PubkeyHex; builderConfig: BuilderConfigData},
533
+ {params: {pubkey: string}; body: unknown},
534
+ EmptyResponseData,
535
+ EmptyMeta
536
+ >;
537
+ /** Remove the builder configuration for a validator public key, it then follows the validator client again */
538
+ deleteBuilderConfig: Endpoint<
539
+ // ⏎
540
+ "DELETE",
541
+ {pubkey: PubkeyHex},
542
+ {params: {pubkey: string}},
543
+ EmptyResponseData,
544
+ EmptyMeta
545
+ >;
546
+
370
547
  getProposerConfig: Endpoint<
371
548
  // ⏎
372
549
  "GET",
@@ -654,6 +831,56 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
654
831
  },
655
832
  resp: EmptyResponseCodec,
656
833
  },
834
+ getBuilderConfig: {
835
+ url: "/eth/v1/validator/{pubkey}/builder_config",
836
+ method: "GET",
837
+ req: {
838
+ writeReq: ({pubkey}) => ({params: {pubkey}}),
839
+ parseReq: ({params: {pubkey}}) => ({pubkey}),
840
+ schema: {
841
+ params: {pubkey: Schema.StringRequired},
842
+ },
843
+ },
844
+ resp: {
845
+ onlySupport: WireFormat.json,
846
+ data: {
847
+ toJson: (data) => builderConfigDataToJson(data),
848
+ fromJson: (data) => builderConfigDataFromJson(data),
849
+ serialize: () => {
850
+ throw Error("SSZ not supported");
851
+ },
852
+ deserialize: () => {
853
+ throw Error("SSZ not supported");
854
+ },
855
+ },
856
+ meta: EmptyMetaCodec,
857
+ },
858
+ },
859
+ setBuilderConfig: {
860
+ url: "/eth/v1/validator/{pubkey}/builder_config",
861
+ method: "POST",
862
+ req: JsonOnlyReq({
863
+ writeReqJson: ({pubkey, builderConfig}) => ({params: {pubkey}, body: builderConfigDataToJson(builderConfig)}),
864
+ parseReqJson: ({params: {pubkey}, body}) => ({pubkey, builderConfig: builderConfigDataFromJson(body)}),
865
+ schema: {
866
+ params: {pubkey: Schema.StringRequired},
867
+ body: Schema.Object,
868
+ },
869
+ }),
870
+ resp: EmptyResponseCodec,
871
+ },
872
+ deleteBuilderConfig: {
873
+ url: "/eth/v1/validator/{pubkey}/builder_config",
874
+ method: "DELETE",
875
+ req: {
876
+ writeReq: ({pubkey}) => ({params: {pubkey}}),
877
+ parseReq: ({params: {pubkey}}) => ({pubkey}),
878
+ schema: {
879
+ params: {pubkey: Schema.StringRequired},
880
+ },
881
+ },
882
+ resp: EmptyResponseCodec,
883
+ },
657
884
 
658
885
  getProposerConfig: {
659
886
  url: "/eth/v0/validator/{pubkey}/proposer_config",
@@ -90,7 +90,7 @@ export class ApiResponse<E extends Endpoint> extends Response {
90
90
  const metaJson = this.definition.resp.transform
91
91
  ? this.definition.resp.transform.fromResponse(rawBody.value).meta
92
92
  : rawBody.value;
93
- this._meta = this.definition.resp.meta.fromJson(metaJson);
93
+ this._meta = this.definition.resp.meta.fromJson(metaJson, new HeadersExtra(this.headers));
94
94
  break;
95
95
  }
96
96
  case WireFormat.ssz:
@@ -80,6 +80,11 @@ export enum MetaHeader {
80
80
  ExecutionPayloadIncluded = "Eth-Execution-Payload-Included",
81
81
  ExecutionPayloadValue = "Eth-Execution-Payload-Value",
82
82
 
83
+ /* Builder API headers */
84
+ BuilderUrl = "Eth-Builder-Url",
85
+ DateMilliseconds = "Date-Milliseconds",
86
+ TimeoutMs = "X-Timeout-Ms",
87
+
83
88
  /* Lodestar-specific (non-standardized) headers */
84
89
  Finalized = "Eth-Consensus-Finalized",
85
90
  DependentRoot = "Eth-Consensus-Dependent-Root",
@@ -126,7 +126,7 @@ export type ResponseDataCodec<T, M> = {
126
126
 
127
127
  export type ResponseMetadataCodec<T> = {
128
128
  toJson: (val: T) => unknown; // server
129
- fromJson: (val: unknown) => T; // client
129
+ fromJson: (val: unknown, headers?: HeadersExtra) => T; // client
130
130
  toHeadersObject: (val: T) => Record<string, string>; // server
131
131
  fromHeaders: (headers: HeadersExtra) => T; // server
132
132
  };