@lodestar/validator 1.47.0-dev.f591cb177e → 1.48.0-dev.67344272c0
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/services/block.d.ts.map +1 -1
- package/lib/services/block.js +33 -3
- package/lib/services/block.js.map +1 -1
- package/lib/services/builderPreferences.d.ts +27 -0
- package/lib/services/builderPreferences.d.ts.map +1 -0
- package/lib/services/builderPreferences.js +127 -0
- package/lib/services/builderPreferences.js.map +1 -0
- package/lib/services/externalSignerSync.js +1 -1
- package/lib/services/externalSignerSync.js.map +1 -1
- package/lib/services/prepareBeaconProposer.d.ts.map +1 -1
- package/lib/services/prepareBeaconProposer.js +5 -2
- package/lib/services/prepareBeaconProposer.js.map +1 -1
- package/lib/services/validatorStore.d.ts +44 -2
- package/lib/services/validatorStore.d.ts.map +1 -1
- package/lib/services/validatorStore.js +196 -28
- package/lib/services/validatorStore.js.map +1 -1
- package/lib/types.d.ts +1 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/util/externalSignerClient.d.ts +5 -1
- package/lib/util/externalSignerClient.d.ts.map +1 -1
- package/lib/util/externalSignerClient.js +4 -0
- package/lib/util/externalSignerClient.js.map +1 -1
- package/lib/validator.d.ts.map +1 -1
- package/lib/validator.js +16 -3
- package/lib/validator.js.map +1 -1
- package/package.json +14 -14
- package/src/services/block.ts +37 -3
- package/src/services/builderPreferences.ts +142 -0
- package/src/services/externalSignerSync.ts +1 -1
- package/src/services/prepareBeaconProposer.ts +5 -2
- package/src/services/validatorStore.ts +268 -33
- package/src/types.ts +1 -1
- package/src/util/externalSignerClient.ts +7 -1
- package/src/validator.ts +30 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {SecretKey} from "@chainsafe/blst";
|
|
1
|
+
import {SecretKey} from "@chainsafe/lodestar-z/blst";
|
|
2
2
|
import {BitArray} from "@chainsafe/ssz";
|
|
3
3
|
import {routes} from "@lodestar/api";
|
|
4
|
+
import {BuilderConfigData, BuilderEntryConfig} from "@lodestar/api/keymanager";
|
|
4
5
|
import {BeaconConfig} from "@lodestar/config";
|
|
5
6
|
import {
|
|
6
7
|
DOMAIN_AGGREGATE_AND_PROOF,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
DOMAIN_BEACON_ATTESTER,
|
|
9
10
|
DOMAIN_BEACON_BUILDER,
|
|
10
11
|
DOMAIN_BEACON_PROPOSER,
|
|
12
|
+
DOMAIN_BUILDER_REQUEST_AUTH,
|
|
11
13
|
DOMAIN_CONTRIBUTION_AND_PROOF,
|
|
12
14
|
DOMAIN_PROPOSER_PREFERENCES,
|
|
13
15
|
DOMAIN_PTC_ATTESTER,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
DOMAIN_SYNC_COMMITTEE,
|
|
17
19
|
DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF,
|
|
18
20
|
ForkSeq,
|
|
21
|
+
MAX_BUILDER_AUTH_DATA_SIZE,
|
|
19
22
|
} from "@lodestar/params";
|
|
20
23
|
import {
|
|
21
24
|
ZERO_HASH,
|
|
@@ -46,7 +49,7 @@ import {
|
|
|
46
49
|
phase0,
|
|
47
50
|
ssz,
|
|
48
51
|
} from "@lodestar/types";
|
|
49
|
-
import {fromHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
|
|
52
|
+
import {fromHex, isValidAsciiHttpUrl, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
|
|
50
53
|
import {Metrics} from "../metrics.js";
|
|
51
54
|
import {ISlashingProtection} from "../slashingProtection/index.js";
|
|
52
55
|
import {PubkeyHex} from "../types.js";
|
|
@@ -84,6 +87,9 @@ type DefaultProposerConfig = {
|
|
|
84
87
|
gasLimit?: number;
|
|
85
88
|
selection?: routes.validator.BuilderSelection;
|
|
86
89
|
boostFactor: bigint;
|
|
90
|
+
minBid: bigint;
|
|
91
|
+
maxExecutionPayment: bigint;
|
|
92
|
+
builders?: BuilderEntryConfig[];
|
|
87
93
|
};
|
|
88
94
|
};
|
|
89
95
|
|
|
@@ -95,9 +101,23 @@ export type ProposerConfig = {
|
|
|
95
101
|
gasLimit?: number;
|
|
96
102
|
selection?: routes.validator.BuilderSelection;
|
|
97
103
|
boostFactor?: bigint;
|
|
104
|
+
minBid?: bigint;
|
|
105
|
+
maxExecutionPayment?: bigint;
|
|
106
|
+
/** Per-key builder entries, replacing the validator client's builders */
|
|
107
|
+
builders?: BuilderEntryConfig[];
|
|
98
108
|
};
|
|
99
109
|
};
|
|
100
110
|
|
|
111
|
+
/** A builder entry with every omitted value resolved against the key and validator client defaults */
|
|
112
|
+
export type ResolvedBuilderEntry = {
|
|
113
|
+
url: string;
|
|
114
|
+
authData: Uint8Array;
|
|
115
|
+
builderPubkeys: Uint8Array[];
|
|
116
|
+
maxExecutionPayment: bigint;
|
|
117
|
+
minBid: bigint;
|
|
118
|
+
builderBoostFactor: bigint;
|
|
119
|
+
};
|
|
120
|
+
|
|
101
121
|
export type ValidatorProposerConfig = {
|
|
102
122
|
proposerConfig: {[index: PubkeyHex]: ProposerConfig};
|
|
103
123
|
defaultConfig: ProposerConfig;
|
|
@@ -132,6 +152,8 @@ export type Signer = SignerLocal | SignerRemote;
|
|
|
132
152
|
type ValidatorData = ProposerConfig & {
|
|
133
153
|
signer: Signer;
|
|
134
154
|
builderData?: BuilderData;
|
|
155
|
+
/** Pre-signed builder request auths keyed by proposal slot and auth data, pruned by proposal slot */
|
|
156
|
+
builderRequestAuths?: Map<string, gloas.SignedBuilderRequestAuth>;
|
|
135
157
|
};
|
|
136
158
|
|
|
137
159
|
export const defaultOptions = {
|
|
@@ -139,7 +161,10 @@ export const defaultOptions = {
|
|
|
139
161
|
defaultGasLimit: 60_000_000,
|
|
140
162
|
builderSelection: routes.validator.BuilderSelection.ExecutionOnly,
|
|
141
163
|
builderAliasSelection: routes.validator.BuilderSelection.Default,
|
|
142
|
-
builderBoostFactor:
|
|
164
|
+
builderBoostFactor: 100n,
|
|
165
|
+
builderMinBid: 0n,
|
|
166
|
+
// Only trustless payments via the builder's staked collateral are counted by default
|
|
167
|
+
builderMaxExecutionPayment: 0n,
|
|
143
168
|
// spec asks for gossip validation by default
|
|
144
169
|
broadcastValidation: routes.beacon.BroadcastValidation.gossip,
|
|
145
170
|
// should request fetching the locally produced block in blinded format
|
|
@@ -148,6 +173,32 @@ export const defaultOptions = {
|
|
|
148
173
|
|
|
149
174
|
export const MAX_BUILDER_BOOST_FACTOR = 2n ** 64n - 1n;
|
|
150
175
|
|
|
176
|
+
/** Pre-Gloas there is no in-protocol builder so the default is local-only, post-Gloas bids are used */
|
|
177
|
+
export function getDefaultBuilderSelection(isPostGloas: boolean): routes.validator.BuilderSelection {
|
|
178
|
+
return isPostGloas ? defaultOptions.builderAliasSelection : defaultOptions.builderSelection;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Boost factor implied by a builder selection, `configuredBoostFactor` only applies to `maxprofit` */
|
|
182
|
+
export function getBuilderBoostFactor(
|
|
183
|
+
selection: routes.validator.BuilderSelection,
|
|
184
|
+
configuredBoostFactor: bigint
|
|
185
|
+
): bigint {
|
|
186
|
+
switch (selection) {
|
|
187
|
+
case routes.validator.BuilderSelection.Default:
|
|
188
|
+
// Default value slightly favors local block to improve censorship resistance of Ethereum
|
|
189
|
+
// The people have spoken and so it shall be https://x.com/lodestar_eth/status/1772679499928191044
|
|
190
|
+
return BigInt(90);
|
|
191
|
+
case routes.validator.BuilderSelection.MaxProfit:
|
|
192
|
+
return configuredBoostFactor;
|
|
193
|
+
case routes.validator.BuilderSelection.BuilderAlways:
|
|
194
|
+
case routes.validator.BuilderSelection.BuilderOnly:
|
|
195
|
+
return MAX_BUILDER_BOOST_FACTOR;
|
|
196
|
+
case routes.validator.BuilderSelection.ExecutionAlways:
|
|
197
|
+
case routes.validator.BuilderSelection.ExecutionOnly:
|
|
198
|
+
return BigInt(0);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
151
202
|
/**
|
|
152
203
|
* Service that sets up and handles validator attester duties.
|
|
153
204
|
*/
|
|
@@ -185,6 +236,9 @@ export class ValidatorStore {
|
|
|
185
236
|
gasLimit: defaultConfig.builder?.gasLimit,
|
|
186
237
|
selection: defaultConfig.builder?.selection,
|
|
187
238
|
boostFactor: builderBoostFactor,
|
|
239
|
+
minBid: defaultConfig.builder?.minBid ?? defaultOptions.builderMinBid,
|
|
240
|
+
maxExecutionPayment: defaultConfig.builder?.maxExecutionPayment ?? defaultOptions.builderMaxExecutionPayment,
|
|
241
|
+
builders: defaultConfig.builder?.builders,
|
|
188
242
|
},
|
|
189
243
|
};
|
|
190
244
|
|
|
@@ -281,17 +335,28 @@ export class ValidatorStore {
|
|
|
281
335
|
|
|
282
336
|
getBuilderSelectionParams(
|
|
283
337
|
pubkeyHex: PubkeyHex,
|
|
284
|
-
slot
|
|
338
|
+
slot: Slot
|
|
285
339
|
): {selection: routes.validator.BuilderSelection; boostFactor: bigint} {
|
|
286
340
|
// Builder bids post-gloas are in-protocol, so the default strategy uses them regardless of
|
|
287
341
|
// whether they are received over p2p or through a builder API. Pre-gloas there is no
|
|
288
342
|
// in-protocol builder, so the default remains local-only (executiononly).
|
|
289
|
-
const isPostGloas =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
343
|
+
const isPostGloas = this.config.getForkSeq(slot) >= ForkSeq.gloas;
|
|
344
|
+
return this.resolveBuilderSelectionParams(pubkeyHex, isPostGloas);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private resolveBuilderSelectionParams(
|
|
348
|
+
pubkeyHex: PubkeyHex,
|
|
349
|
+
isPostGloas: boolean
|
|
350
|
+
): {selection: routes.validator.BuilderSelection; boostFactor: bigint} {
|
|
351
|
+
const validatorBuilder = this.validators.get(pubkeyHex)?.builder;
|
|
352
|
+
const defaultSelection = getDefaultBuilderSelection(isPostGloas);
|
|
353
|
+
let selection = validatorBuilder?.selection ?? this.defaultProposerConfig.builder.selection ?? defaultSelection;
|
|
354
|
+
|
|
355
|
+
// The standard per-key builder config directly controls the post-Gloas boost. It takes
|
|
356
|
+
// precedence over the legacy selection aliases when explicitly configured.
|
|
357
|
+
if (isPostGloas && validatorBuilder?.boostFactor !== undefined) {
|
|
358
|
+
return {selection: routes.validator.BuilderSelection.MaxProfit, boostFactor: validatorBuilder.boostFactor};
|
|
359
|
+
}
|
|
295
360
|
|
|
296
361
|
// Post-Gloas block production uses standard builder boost factor. Need to normalize the
|
|
297
362
|
// gloas-deprecated "builderonly" and "executiononly" to the gloas fallback "builderalways"
|
|
@@ -304,28 +369,10 @@ export class ValidatorStore {
|
|
|
304
369
|
}
|
|
305
370
|
}
|
|
306
371
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// The people have spoken and so it shall be https://x.com/lodestar_eth/status/1772679499928191044
|
|
312
|
-
boostFactor = BigInt(90);
|
|
313
|
-
break;
|
|
314
|
-
|
|
315
|
-
case routes.validator.BuilderSelection.MaxProfit:
|
|
316
|
-
boostFactor =
|
|
317
|
-
this.validators.get(pubkeyHex)?.builder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor;
|
|
318
|
-
break;
|
|
319
|
-
|
|
320
|
-
case routes.validator.BuilderSelection.BuilderAlways:
|
|
321
|
-
case routes.validator.BuilderSelection.BuilderOnly:
|
|
322
|
-
boostFactor = MAX_BUILDER_BOOST_FACTOR;
|
|
323
|
-
break;
|
|
324
|
-
|
|
325
|
-
case routes.validator.BuilderSelection.ExecutionAlways:
|
|
326
|
-
case routes.validator.BuilderSelection.ExecutionOnly:
|
|
327
|
-
boostFactor = BigInt(0);
|
|
328
|
-
}
|
|
372
|
+
const boostFactor = getBuilderBoostFactor(
|
|
373
|
+
selection,
|
|
374
|
+
validatorBuilder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor
|
|
375
|
+
);
|
|
329
376
|
|
|
330
377
|
return {selection, boostFactor};
|
|
331
378
|
}
|
|
@@ -409,6 +456,126 @@ export class ValidatorStore {
|
|
|
409
456
|
delete validatorData.builder?.boostFactor;
|
|
410
457
|
}
|
|
411
458
|
|
|
459
|
+
getBuilderMinBid(pubkeyHex: PubkeyHex): bigint {
|
|
460
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
461
|
+
if (validatorData === undefined) {
|
|
462
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
463
|
+
}
|
|
464
|
+
return validatorData?.builder?.minBid ?? this.defaultProposerConfig.builder.minBid;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
getBuilderMaxExecutionPayment(pubkeyHex: PubkeyHex): bigint {
|
|
468
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
469
|
+
if (validatorData === undefined) {
|
|
470
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
471
|
+
}
|
|
472
|
+
return validatorData?.builder?.maxExecutionPayment ?? this.defaultProposerConfig.builder.maxExecutionPayment;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Resolve the builder entries for this key. Per-key entries replace the validator client's
|
|
477
|
+
* builders. A value omitted on an entry takes this key's default, then the validator
|
|
478
|
+
* client's configuration, while omitted auth data is derived from the entry url instead.
|
|
479
|
+
*/
|
|
480
|
+
getResolvedBuilderEntries(pubkeyHex: PubkeyHex, boostFactor?: bigint): ResolvedBuilderEntry[] {
|
|
481
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
482
|
+
if (validatorData === undefined) {
|
|
483
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const keyMinBid = validatorData.builder?.minBid ?? this.defaultProposerConfig.builder.minBid;
|
|
487
|
+
const keyBoostFactor =
|
|
488
|
+
boostFactor ?? validatorData.builder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor;
|
|
489
|
+
const keyMaxExecutionPayment =
|
|
490
|
+
validatorData.builder?.maxExecutionPayment ?? this.defaultProposerConfig.builder.maxExecutionPayment;
|
|
491
|
+
|
|
492
|
+
// The key's defaults apply to the validator client's own builders all the same
|
|
493
|
+
const builders = validatorData.builder?.builders ?? this.defaultProposerConfig.builder.builders ?? [];
|
|
494
|
+
return builders.map((entry) => ({
|
|
495
|
+
url: entry.url,
|
|
496
|
+
authData: entry.authData !== undefined ? fromHex(entry.authData) : new TextEncoder().encode(entry.url),
|
|
497
|
+
builderPubkeys: (entry.builderPubkeys ?? []).map(fromHex),
|
|
498
|
+
maxExecutionPayment: entry.maxExecutionPayment ?? keyMaxExecutionPayment,
|
|
499
|
+
minBid: entry.minBid ?? keyMinBid,
|
|
500
|
+
builderBoostFactor: entry.builderBoostFactor ?? keyBoostFactor,
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Return the builder configuration in effect for this key, with omitted values resolved */
|
|
505
|
+
getBuilderConfig(pubkeyHex: PubkeyHex): BuilderConfigData {
|
|
506
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
507
|
+
if (validatorData === undefined) {
|
|
508
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
509
|
+
}
|
|
510
|
+
const {boostFactor} = this.resolveBuilderSelectionParams(pubkeyHex, true);
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
minBid: validatorData.builder?.minBid ?? this.defaultProposerConfig.builder.minBid,
|
|
514
|
+
builderBoostFactor: boostFactor,
|
|
515
|
+
builders: this.getResolvedBuilderEntries(pubkeyHex, boostFactor).map((entry) => ({
|
|
516
|
+
url: entry.url,
|
|
517
|
+
authData: toHex(entry.authData),
|
|
518
|
+
builderPubkeys: entry.builderPubkeys.map(toPubkeyHex),
|
|
519
|
+
maxExecutionPayment: entry.maxExecutionPayment,
|
|
520
|
+
minBid: entry.minBid,
|
|
521
|
+
builderBoostFactor: entry.builderBoostFactor,
|
|
522
|
+
})),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Set the builder configuration for this key, replacing any stored configuration in full */
|
|
527
|
+
setBuilderConfig(pubkeyHex: PubkeyHex, config: BuilderConfigData): void {
|
|
528
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
529
|
+
if (validatorData === undefined) {
|
|
530
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for (const value of [config.minBid, config.builderBoostFactor]) {
|
|
534
|
+
if (value !== undefined && value > MAX_BUILDER_BOOST_FACTOR) {
|
|
535
|
+
throw Error(`Invalid builder config value=${value} exceeds uint64`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// No two entries may share both their url and their auth data, an omitted auth data is
|
|
540
|
+
// compared as the value derived from the entry url
|
|
541
|
+
const seenEntries = new Set<string>();
|
|
542
|
+
for (const entry of config.builders ?? []) {
|
|
543
|
+
if (!isValidAsciiHttpUrl(entry.url)) {
|
|
544
|
+
throw Error(`Invalid builder url: ${entry.url}`);
|
|
545
|
+
}
|
|
546
|
+
const authData =
|
|
547
|
+
entry.authData !== undefined ? toHex(fromHex(entry.authData)) : toHex(new TextEncoder().encode(entry.url));
|
|
548
|
+
const entryKey = `${entry.url}|${authData}`;
|
|
549
|
+
if (seenEntries.has(entryKey)) {
|
|
550
|
+
throw Error(`Duplicate builder entry url=${entry.url} authData=${authData}`);
|
|
551
|
+
}
|
|
552
|
+
seenEntries.add(entryKey);
|
|
553
|
+
for (const value of [entry.maxExecutionPayment, entry.minBid, entry.builderBoostFactor]) {
|
|
554
|
+
if (value !== undefined && value > MAX_BUILDER_BOOST_FACTOR) {
|
|
555
|
+
throw Error(`Invalid builder entry value=${value} exceeds uint64`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
validatorData.builder = {
|
|
561
|
+
...validatorData.builder,
|
|
562
|
+
minBid: config.minBid,
|
|
563
|
+
boostFactor: config.builderBoostFactor,
|
|
564
|
+
builders: config.builders,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Remove the builder configuration for this key, and revert to the validator client configuration */
|
|
569
|
+
deleteBuilderConfig(pubkeyHex: PubkeyHex): void {
|
|
570
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
571
|
+
if (validatorData === undefined) {
|
|
572
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
573
|
+
}
|
|
574
|
+
delete validatorData.builder?.minBid;
|
|
575
|
+
delete validatorData.builder?.boostFactor;
|
|
576
|
+
delete validatorData.builder?.builders;
|
|
577
|
+
}
|
|
578
|
+
|
|
412
579
|
/** Return true if `index` is active part of this validator client */
|
|
413
580
|
hasValidatorIndex(index: ValidatorIndex): boolean {
|
|
414
581
|
return this.indicesService.index2pubkey.has(index);
|
|
@@ -430,7 +597,10 @@ export class ValidatorStore {
|
|
|
430
597
|
feeRecipient !== undefined ||
|
|
431
598
|
builder?.gasLimit !== undefined ||
|
|
432
599
|
builder?.selection !== undefined ||
|
|
433
|
-
builder?.boostFactor !== undefined
|
|
600
|
+
builder?.boostFactor !== undefined ||
|
|
601
|
+
builder?.minBid !== undefined ||
|
|
602
|
+
builder?.maxExecutionPayment !== undefined ||
|
|
603
|
+
builder?.builders !== undefined
|
|
434
604
|
) {
|
|
435
605
|
proposerConfig = {graffiti, strictFeeRecipientCheck, feeRecipient, builder};
|
|
436
606
|
}
|
|
@@ -879,6 +1049,71 @@ export class ValidatorStore {
|
|
|
879
1049
|
};
|
|
880
1050
|
}
|
|
881
1051
|
|
|
1052
|
+
async signBuilderRequestAuth(
|
|
1053
|
+
pubkeyMaybeHex: BLSPubkeyMaybeHex,
|
|
1054
|
+
data: Uint8Array,
|
|
1055
|
+
proposalSlot: Slot
|
|
1056
|
+
): Promise<gloas.SignedBuilderRequestAuth> {
|
|
1057
|
+
if (data.length === 0 || data.length > MAX_BUILDER_AUTH_DATA_SIZE) {
|
|
1058
|
+
throw Error(
|
|
1059
|
+
`Invalid builder request auth data length=${data.length}, must be within 1 and ${MAX_BUILDER_AUTH_DATA_SIZE} bytes`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const message: gloas.BuilderRequestAuth = {data, slot: proposalSlot};
|
|
1064
|
+
|
|
1065
|
+
const signingSlot = 0;
|
|
1066
|
+
const domain = computeDomain(DOMAIN_BUILDER_REQUEST_AUTH, this.config.GENESIS_FORK_VERSION, ZERO_HASH);
|
|
1067
|
+
const signingRoot = computeSigningRoot(ssz.gloas.BuilderRequestAuth, message, domain);
|
|
1068
|
+
|
|
1069
|
+
const signableMessage: SignableMessage = {
|
|
1070
|
+
type: SignableMessageType.BUILDER_REQUEST_AUTH,
|
|
1071
|
+
data: message,
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
message,
|
|
1076
|
+
signature: await this.getSignature(pubkeyMaybeHex, signingRoot, signingSlot, signableMessage),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Return a pre-signed builder request auth for the auth data and proposal slot, or sign and cache a new
|
|
1082
|
+
* one. Signing happens off the block proposal hot path when preferences are submitted ahead of
|
|
1083
|
+
* time, cached auths are then used just-in-time when requesting bids at proposal time.
|
|
1084
|
+
*/
|
|
1085
|
+
async getBuilderRequestAuth(
|
|
1086
|
+
pubkeyMaybeHex: BLSPubkeyMaybeHex,
|
|
1087
|
+
data: Uint8Array,
|
|
1088
|
+
proposalSlot: Slot,
|
|
1089
|
+
currentSlot: Slot
|
|
1090
|
+
): Promise<gloas.SignedBuilderRequestAuth> {
|
|
1091
|
+
const pubkeyHex = typeof pubkeyMaybeHex === "string" ? pubkeyMaybeHex : toPubkeyHex(pubkeyMaybeHex);
|
|
1092
|
+
const authKey = `${proposalSlot}-${toHex(data)}`;
|
|
1093
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
1094
|
+
const cached = validatorData?.builderRequestAuths?.get(authKey);
|
|
1095
|
+
if (cached !== undefined) {
|
|
1096
|
+
return cached;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const signedRequestAuth = await this.signBuilderRequestAuth(pubkeyMaybeHex, data, proposalSlot);
|
|
1100
|
+
|
|
1101
|
+
if (validatorData !== undefined) {
|
|
1102
|
+
const builderRequestAuths =
|
|
1103
|
+
validatorData.builderRequestAuths ?? new Map<string, gloas.SignedBuilderRequestAuth>();
|
|
1104
|
+
// Prune auths for proposal slots that are already in the past
|
|
1105
|
+
for (const key of builderRequestAuths.keys()) {
|
|
1106
|
+
if (Number(key.slice(0, key.indexOf("-"))) < currentSlot) {
|
|
1107
|
+
builderRequestAuths.delete(key);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
builderRequestAuths.set(authKey, signedRequestAuth);
|
|
1111
|
+
validatorData.builderRequestAuths = builderRequestAuths;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return signedRequestAuth;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
882
1117
|
async getValidatorRegistration(
|
|
883
1118
|
pubkeyMaybeHex: BLSPubkeyMaybeHex,
|
|
884
1119
|
regAttributes: {feeRecipient: ExecutionAddress; gasLimit: number},
|
package/src/types.ts
CHANGED
|
@@ -36,6 +36,7 @@ export enum SignableMessageType {
|
|
|
36
36
|
EXECUTION_PAYLOAD_ENVELOPE = "EXECUTION_PAYLOAD_ENVELOPE",
|
|
37
37
|
PAYLOAD_ATTESTATION = "PAYLOAD_ATTESTATION",
|
|
38
38
|
PROPOSER_PREFERENCES = "PROPOSER_PREFERENCES",
|
|
39
|
+
BUILDER_REQUEST_AUTH = "BUILDER_REQUEST_AUTH",
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const AggregationSlotType = new ContainerType({
|
|
@@ -87,7 +88,8 @@ export type SignableMessage =
|
|
|
87
88
|
| {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1}
|
|
88
89
|
| {type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE; data: gloas.ExecutionPayloadEnvelope}
|
|
89
90
|
| {type: SignableMessageType.PAYLOAD_ATTESTATION; data: gloas.PayloadAttestationData}
|
|
90
|
-
| {type: SignableMessageType.PROPOSER_PREFERENCES; data: gloas.ProposerPreferences}
|
|
91
|
+
| {type: SignableMessageType.PROPOSER_PREFERENCES; data: gloas.ProposerPreferences}
|
|
92
|
+
| {type: SignableMessageType.BUILDER_REQUEST_AUTH; data: gloas.BuilderRequestAuth};
|
|
91
93
|
|
|
92
94
|
const requiresForkInfo: Record<SignableMessageType, boolean> = {
|
|
93
95
|
[SignableMessageType.AGGREGATION_SLOT]: true,
|
|
@@ -105,6 +107,7 @@ const requiresForkInfo: Record<SignableMessageType, boolean> = {
|
|
|
105
107
|
[SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE]: true,
|
|
106
108
|
[SignableMessageType.PAYLOAD_ATTESTATION]: true,
|
|
107
109
|
[SignableMessageType.PROPOSER_PREFERENCES]: true,
|
|
110
|
+
[SignableMessageType.BUILDER_REQUEST_AUTH]: false,
|
|
108
111
|
};
|
|
109
112
|
|
|
110
113
|
type Web3SignerSerializedRequest = {
|
|
@@ -285,6 +288,9 @@ function serializerSignableMessagePayload(config: BeaconConfig, payload: Signabl
|
|
|
285
288
|
|
|
286
289
|
case SignableMessageType.PROPOSER_PREFERENCES:
|
|
287
290
|
return {proposer_preferences: ssz.gloas.ProposerPreferences.toJson(payload.data)};
|
|
291
|
+
|
|
292
|
+
case SignableMessageType.BUILDER_REQUEST_AUTH:
|
|
293
|
+
return {builder_request_auth: ssz.gloas.BuilderRequestAuth.toJson(payload.data)};
|
|
288
294
|
}
|
|
289
295
|
}
|
|
290
296
|
|
package/src/validator.ts
CHANGED
|
@@ -9,13 +9,14 @@ import {
|
|
|
9
9
|
import {Clock, ClockOptions, IClock, computeEpochAtSlot, getCurrentSlot} from "@lodestar/state-transition";
|
|
10
10
|
import {BLSPubkey, phase0, ssz} from "@lodestar/types";
|
|
11
11
|
import {Genesis} from "@lodestar/types/phase0";
|
|
12
|
-
import {Logger, toPrintableUrl, toRootHex} from "@lodestar/utils";
|
|
12
|
+
import {Logger, prettyGweiToEth, toPrintableUrl, toRootHex} from "@lodestar/utils";
|
|
13
13
|
import {waitForGenesis} from "./genesis.js";
|
|
14
14
|
import {Metrics} from "./metrics.js";
|
|
15
15
|
import {MetaDataRepository} from "./repositories/metaDataRepository.js";
|
|
16
16
|
import {AttestationService} from "./services/attestation.js";
|
|
17
17
|
import {BlockProposingService} from "./services/block.js";
|
|
18
18
|
import {BlockDutiesService} from "./services/blockDuties.js";
|
|
19
|
+
import {BuilderPreferencesService} from "./services/builderPreferences.js";
|
|
19
20
|
import {ChainHeaderTracker} from "./services/chainHeaderTracker.js";
|
|
20
21
|
import {DoppelgangerService} from "./services/doppelgangerService.js";
|
|
21
22
|
import {ValidatorEventEmitter} from "./services/emitter.js";
|
|
@@ -26,7 +27,14 @@ import {ProposerPreferencesService} from "./services/proposerPreferences.js";
|
|
|
26
27
|
import {PtcService} from "./services/ptc.js";
|
|
27
28
|
import {SyncCommitteeService} from "./services/syncCommittee.js";
|
|
28
29
|
import {SyncingStatusTracker} from "./services/syncingStatusTracker.js";
|
|
29
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
Signer,
|
|
32
|
+
ValidatorProposerConfig,
|
|
33
|
+
ValidatorStore,
|
|
34
|
+
defaultOptions,
|
|
35
|
+
getBuilderBoostFactor,
|
|
36
|
+
getDefaultBuilderSelection,
|
|
37
|
+
} from "./services/validatorStore.js";
|
|
30
38
|
import {ISlashingProtection, Interchange, InterchangeFormatVersion} from "./slashingProtection/index.js";
|
|
31
39
|
import {LodestarValidatorDatabaseController, ProcessShutdownCallback, PubkeyHex} from "./types.js";
|
|
32
40
|
import {getLoggerVc} from "./util/index.js";
|
|
@@ -313,6 +321,7 @@ export class Validator {
|
|
|
313
321
|
);
|
|
314
322
|
|
|
315
323
|
new ProposerPreferencesService(config, loggerVc, api, clock, validatorStore, blockDutiesService, metrics);
|
|
324
|
+
new BuilderPreferencesService(config, loggerVc, api, clock, validatorStore, blockDutiesService, metrics);
|
|
316
325
|
|
|
317
326
|
return new Validator({
|
|
318
327
|
opts,
|
|
@@ -367,8 +376,9 @@ export class Validator {
|
|
|
367
376
|
logger.info("Verified connected beacon node and validator have the same genesisValidatorRoot");
|
|
368
377
|
|
|
369
378
|
const {broadcastValidation = defaultOptions.broadcastValidation, valProposerConfig} = opts;
|
|
379
|
+
const gloasScheduled = config.GLOAS_FORK_EPOCH !== Infinity;
|
|
370
380
|
const defaultBuilderSelection =
|
|
371
|
-
valProposerConfig?.defaultConfig.builder?.selection ??
|
|
381
|
+
valProposerConfig?.defaultConfig.builder?.selection ?? getDefaultBuilderSelection(gloasScheduled);
|
|
372
382
|
const strictFeeRecipientCheck = valProposerConfig?.defaultConfig.strictFeeRecipientCheck ?? false;
|
|
373
383
|
const suggestedFeeRecipient = valProposerConfig?.defaultConfig.feeRecipient ?? defaultOptions.suggestedFeeRecipient;
|
|
374
384
|
|
|
@@ -381,6 +391,23 @@ export class Validator {
|
|
|
381
391
|
|
|
382
392
|
metrics?.defaultConfiguration.set({builderSelection: defaultBuilderSelection, broadcastValidation}, 1);
|
|
383
393
|
|
|
394
|
+
if (gloasScheduled) {
|
|
395
|
+
const defaultBuilderConfig = valProposerConfig?.defaultConfig.builder;
|
|
396
|
+
const defaultBuilders = defaultBuilderConfig?.builders ?? [];
|
|
397
|
+
|
|
398
|
+
logger.info("Builder config", {
|
|
399
|
+
builders: defaultBuilders.map((entry) => toPrintableUrl(entry.url)).join(",") || "p2p only",
|
|
400
|
+
boostFactor: getBuilderBoostFactor(
|
|
401
|
+
defaultBuilderSelection,
|
|
402
|
+
defaultBuilderConfig?.boostFactor ?? defaultOptions.builderBoostFactor
|
|
403
|
+
),
|
|
404
|
+
minBid: prettyGweiToEth(defaultBuilderConfig?.minBid ?? defaultOptions.builderMinBid),
|
|
405
|
+
maxExecutionPayment: prettyGweiToEth(
|
|
406
|
+
defaultBuilderConfig?.maxExecutionPayment ?? defaultOptions.builderMaxExecutionPayment
|
|
407
|
+
),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
384
411
|
// Instantiates block and attestation services and runs them once the chain has been started.
|
|
385
412
|
return Validator.init(opts, genesis, metrics);
|
|
386
413
|
}
|