@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.
- package/lib/beacon/routes/beacon/block.d.ts +7 -0
- package/lib/beacon/routes/beacon/block.d.ts.map +1 -1
- package/lib/beacon/routes/beacon/block.js +7 -3
- package/lib/beacon/routes/beacon/block.js.map +1 -1
- package/lib/beacon/routes/debug.d.ts +3 -3
- package/lib/beacon/routes/debug.js +3 -3
- package/lib/beacon/routes/debug.js.map +1 -1
- package/lib/beacon/routes/validator.d.ts +92 -5
- package/lib/beacon/routes/validator.d.ts.map +1 -1
- package/lib/beacon/routes/validator.js +121 -16
- package/lib/beacon/routes/validator.js.map +1 -1
- package/lib/beacon/server/events.d.ts.map +1 -1
- package/lib/beacon/server/events.js +10 -2
- package/lib/beacon/server/events.js.map +1 -1
- package/lib/builder/routes.d.ts +48 -1
- package/lib/builder/routes.d.ts.map +1 -1
- package/lib/builder/routes.js +173 -1
- package/lib/builder/routes.js.map +1 -1
- package/lib/keymanager/index.d.ts +2 -2
- package/lib/keymanager/index.d.ts.map +1 -1
- package/lib/keymanager/index.js +1 -1
- package/lib/keymanager/index.js.map +1 -1
- package/lib/keymanager/routes.d.ts +63 -0
- package/lib/keymanager/routes.d.ts.map +1 -1
- package/lib/keymanager/routes.js +147 -0
- package/lib/keymanager/routes.js.map +1 -1
- package/lib/utils/client/response.js +1 -1
- package/lib/utils/client/response.js.map +1 -1
- package/lib/utils/metadata.d.ts +3 -0
- package/lib/utils/metadata.d.ts.map +1 -1
- package/lib/utils/metadata.js +4 -0
- package/lib/utils/metadata.js.map +1 -1
- package/lib/utils/types.d.ts +1 -1
- package/lib/utils/types.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/beacon/routes/beacon/block.ts +18 -4
- package/src/beacon/routes/debug.ts +3 -3
- package/src/beacon/routes/validator.ts +178 -20
- package/src/beacon/server/events.ts +10 -3
- package/src/builder/routes.ts +227 -2
- package/src/keymanager/index.ts +9 -1
- package/src/keymanager/routes.ts +227 -0
- package/src/utils/client/response.ts +1 -1
- package/src/utils/metadata.ts +5 -0
- package/src/utils/types.ts +1 -1
package/src/builder/routes.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/keymanager/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
package/src/keymanager/routes.ts
CHANGED
|
@@ -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:
|
package/src/utils/metadata.ts
CHANGED
|
@@ -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",
|
package/src/utils/types.ts
CHANGED
|
@@ -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
|
};
|