@lodestar/validator 1.47.0-dev.d2f4b96574 → 1.47.0-dev.d79d81f994
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 +43 -5
- 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 +6 -3
- package/lib/services/prepareBeaconProposer.js.map +1 -1
- package/lib/services/proposerPreferences.d.ts +2 -2
- package/lib/services/proposerPreferences.d.ts.map +1 -1
- package/lib/services/proposerPreferences.js +16 -4
- package/lib/services/proposerPreferences.js.map +1 -1
- package/lib/services/validatorStore.d.ts +46 -3
- package/lib/services/validatorStore.d.ts.map +1 -1
- package/lib/services/validatorStore.js +228 -34
- 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 +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 +47 -5
- package/src/services/builderPreferences.ts +142 -0
- package/src/services/externalSignerSync.ts +1 -1
- package/src/services/prepareBeaconProposer.ts +6 -3
- package/src/services/proposerPreferences.ts +17 -4
- package/src/services/validatorStore.ts +307 -42
- package/src/types.ts +1 -1
- package/src/util/externalSignerClient.ts +7 -1
- package/src/validator.ts +31 -4
|
@@ -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";
|
|
@@ -80,10 +83,13 @@ type DefaultProposerConfig = {
|
|
|
80
83
|
strictFeeRecipientCheck: boolean;
|
|
81
84
|
feeRecipient: ExecutionAddress;
|
|
82
85
|
builder: {
|
|
83
|
-
gasLimit: number;
|
|
84
86
|
// Left undefined when not configured so the fork-appropriate default can be resolved per slot
|
|
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
|
*/
|
|
@@ -182,9 +233,12 @@ export class ValidatorStore {
|
|
|
182
233
|
strictFeeRecipientCheck: defaultConfig.strictFeeRecipientCheck ?? false,
|
|
183
234
|
feeRecipient: defaultConfig.feeRecipient ?? defaultOptions.suggestedFeeRecipient,
|
|
184
235
|
builder: {
|
|
185
|
-
gasLimit: defaultConfig.builder?.gasLimit
|
|
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,43 +335,45 @@ 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
|
-
// Builder bids post-gloas are in-protocol
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
case routes.validator.BuilderSelection.MaxProfit:
|
|
307
|
-
boostFactor =
|
|
308
|
-
this.validators.get(pubkeyHex)?.builder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor;
|
|
309
|
-
break;
|
|
310
|
-
|
|
311
|
-
case routes.validator.BuilderSelection.BuilderAlways:
|
|
312
|
-
case routes.validator.BuilderSelection.BuilderOnly:
|
|
313
|
-
boostFactor = MAX_BUILDER_BOOST_FACTOR;
|
|
314
|
-
break;
|
|
315
|
-
|
|
316
|
-
case routes.validator.BuilderSelection.ExecutionAlways:
|
|
317
|
-
case routes.validator.BuilderSelection.ExecutionOnly:
|
|
318
|
-
boostFactor = BigInt(0);
|
|
340
|
+
// Builder bids post-gloas are in-protocol, so the default strategy uses them regardless of
|
|
341
|
+
// whether they are received over p2p or through a builder API. Pre-gloas there is no
|
|
342
|
+
// in-protocol builder, so the default remains local-only (executiononly).
|
|
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};
|
|
319
359
|
}
|
|
320
360
|
|
|
361
|
+
// Post-Gloas block production uses standard builder boost factor. Need to normalize the
|
|
362
|
+
// gloas-deprecated "builderonly" and "executiononly" to the gloas fallback "builderalways"
|
|
363
|
+
// and "executionalways" equivalent before deriving the boost factor.
|
|
364
|
+
if (isPostGloas) {
|
|
365
|
+
if (selection === routes.validator.BuilderSelection.BuilderOnly) {
|
|
366
|
+
selection = routes.validator.BuilderSelection.BuilderAlways;
|
|
367
|
+
} else if (selection === routes.validator.BuilderSelection.ExecutionOnly) {
|
|
368
|
+
selection = routes.validator.BuilderSelection.ExecutionAlways;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const boostFactor = getBuilderBoostFactor(
|
|
373
|
+
selection,
|
|
374
|
+
validatorBuilder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor
|
|
375
|
+
);
|
|
376
|
+
|
|
321
377
|
return {selection, boostFactor};
|
|
322
378
|
}
|
|
323
379
|
|
|
@@ -327,12 +383,33 @@ export class ValidatorStore {
|
|
|
327
383
|
);
|
|
328
384
|
}
|
|
329
385
|
|
|
330
|
-
|
|
386
|
+
getConfiguredDefaultGasLimit(): number | undefined {
|
|
387
|
+
return this.defaultProposerConfig.builder.gasLimit;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
getGasLimit(pubkeyHex: PubkeyHex, slot: Slot, logger?: LoggerVc): number {
|
|
331
391
|
const validatorData = this.validators.get(pubkeyHex);
|
|
332
392
|
if (validatorData === undefined) {
|
|
333
393
|
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
334
394
|
}
|
|
335
|
-
|
|
395
|
+
|
|
396
|
+
const configuredGasLimit = validatorData.builder?.gasLimit ?? this.defaultProposerConfig.builder.gasLimit;
|
|
397
|
+
const scheduledGasLimit = this.config.getScheduledGasLimit(computeEpochAtSlot(slot));
|
|
398
|
+
|
|
399
|
+
if (configuredGasLimit !== undefined) {
|
|
400
|
+
if (scheduledGasLimit !== undefined && configuredGasLimit > scheduledGasLimit) {
|
|
401
|
+
logger?.warn("Configured gas limit exceeds recommended maximum", {
|
|
402
|
+
pubkey: pubkeyHex,
|
|
403
|
+
slot,
|
|
404
|
+
configuredGasLimit,
|
|
405
|
+
recommendedGasLimit: scheduledGasLimit,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return configuredGasLimit;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return scheduledGasLimit ?? defaultOptions.defaultGasLimit;
|
|
336
413
|
}
|
|
337
414
|
|
|
338
415
|
setGasLimit(pubkeyHex: PubkeyHex, gasLimit: number): void {
|
|
@@ -379,6 +456,126 @@ export class ValidatorStore {
|
|
|
379
456
|
delete validatorData.builder?.boostFactor;
|
|
380
457
|
}
|
|
381
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
|
+
|
|
382
579
|
/** Return true if `index` is active part of this validator client */
|
|
383
580
|
hasValidatorIndex(index: ValidatorIndex): boolean {
|
|
384
581
|
return this.indicesService.index2pubkey.has(index);
|
|
@@ -400,7 +597,10 @@ export class ValidatorStore {
|
|
|
400
597
|
feeRecipient !== undefined ||
|
|
401
598
|
builder?.gasLimit !== undefined ||
|
|
402
599
|
builder?.selection !== undefined ||
|
|
403
|
-
builder?.boostFactor !== undefined
|
|
600
|
+
builder?.boostFactor !== undefined ||
|
|
601
|
+
builder?.minBid !== undefined ||
|
|
602
|
+
builder?.maxExecutionPayment !== undefined ||
|
|
603
|
+
builder?.builders !== undefined
|
|
404
604
|
) {
|
|
405
605
|
proposerConfig = {graffiti, strictFeeRecipientCheck, feeRecipient, builder};
|
|
406
606
|
}
|
|
@@ -849,6 +1049,71 @@ export class ValidatorStore {
|
|
|
849
1049
|
};
|
|
850
1050
|
}
|
|
851
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
|
+
|
|
852
1117
|
async getValidatorRegistration(
|
|
853
1118
|
pubkeyMaybeHex: BLSPubkeyMaybeHex,
|
|
854
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";
|
|
@@ -98,7 +106,7 @@ export class Validator {
|
|
|
98
106
|
private readonly syncCommitteeService: SyncCommitteeService;
|
|
99
107
|
private readonly config: BeaconConfig;
|
|
100
108
|
private readonly api: ApiClient;
|
|
101
|
-
|
|
109
|
+
readonly clock: IClock;
|
|
102
110
|
private readonly chainHeaderTracker: ChainHeaderTracker;
|
|
103
111
|
readonly syncingStatusTracker: SyncingStatusTracker;
|
|
104
112
|
private readonly logger: Logger;
|
|
@@ -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
|
}
|