@lodestar/validator 1.47.0-dev.9d19aee2e7 → 1.47.0-dev.9ecc10f386
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 +31 -1
- 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/validatorStore.d.ts +39 -1
- package/lib/services/validatorStore.d.ts.map +1 -1
- package/lib/services/validatorStore.js +173 -9
- 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 +2 -0
- package/lib/validator.js.map +1 -1
- package/package.json +14 -14
- package/src/services/block.ts +35 -1
- package/src/services/builderPreferences.ts +142 -0
- package/src/services/externalSignerSync.ts +1 -1
- package/src/services/validatorStore.ts +236 -10
- package/src/types.ts +1 -1
- package/src/util/externalSignerClient.ts +7 -1
- package/src/validator.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lodestar/validator",
|
|
3
|
-
"version": "1.47.0-dev.
|
|
3
|
+
"version": "1.47.0-dev.9ecc10f386",
|
|
4
4
|
"description": "A Typescript implementation of the validator client",
|
|
5
5
|
"author": "ChainSafe Systems",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,23 +48,23 @@
|
|
|
48
48
|
"blockchain"
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@chainsafe/
|
|
52
|
-
"@chainsafe/ssz": "^1.6.
|
|
53
|
-
"@lodestar/api": "^1.47.0-dev.
|
|
54
|
-
"@lodestar/config": "^1.47.0-dev.
|
|
55
|
-
"@lodestar/db": "^1.47.0-dev.
|
|
56
|
-
"@lodestar/params": "^1.47.0-dev.
|
|
57
|
-
"@lodestar/state-transition": "^1.47.0-dev.
|
|
58
|
-
"@lodestar/types": "^1.47.0-dev.
|
|
59
|
-
"@lodestar/utils": "^1.47.0-dev.
|
|
51
|
+
"@chainsafe/lodestar-z": "catalog:",
|
|
52
|
+
"@chainsafe/ssz": "^1.6.3",
|
|
53
|
+
"@lodestar/api": "^1.47.0-dev.9ecc10f386",
|
|
54
|
+
"@lodestar/config": "^1.47.0-dev.9ecc10f386",
|
|
55
|
+
"@lodestar/db": "^1.47.0-dev.9ecc10f386",
|
|
56
|
+
"@lodestar/params": "^1.47.0-dev.9ecc10f386",
|
|
57
|
+
"@lodestar/state-transition": "^1.47.0-dev.9ecc10f386",
|
|
58
|
+
"@lodestar/types": "^1.47.0-dev.9ecc10f386",
|
|
59
|
+
"@lodestar/utils": "^1.47.0-dev.9ecc10f386",
|
|
60
60
|
"strict-event-emitter-types": "^2.0.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@lodestar/logger": "^1.47.0-dev.
|
|
64
|
-
"@lodestar/spec-test-util": "^1.47.0-dev.
|
|
65
|
-
"@lodestar/test-utils": "^1.47.0-dev.
|
|
63
|
+
"@lodestar/logger": "^1.47.0-dev.9ecc10f386",
|
|
64
|
+
"@lodestar/spec-test-util": "^1.47.0-dev.9ecc10f386",
|
|
65
|
+
"@lodestar/test-utils": "^1.47.0-dev.9ecc10f386",
|
|
66
66
|
"@vekexasia/bigint-buffer2": "^1.1.1",
|
|
67
67
|
"rimraf": "^4.4.1"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "d9f5a350881754b896bbbc33887c8c22ad93acff"
|
|
70
70
|
}
|
package/src/services/block.ts
CHANGED
|
@@ -194,6 +194,8 @@ export class BlockProposingService {
|
|
|
194
194
|
const {broadcastValidation, payloadLocal} = this.opts;
|
|
195
195
|
const {selection: builderSelection, boostFactor: builderBoostFactor} =
|
|
196
196
|
this.validatorStore.getBuilderSelectionParams(pubkeyHex, slot);
|
|
197
|
+
const builderMinBid = this.validatorStore.getBuilderMinBid(pubkeyHex);
|
|
198
|
+
const builderEntries = this.validatorStore.getResolvedBuilderEntries(pubkeyHex, builderBoostFactor);
|
|
197
199
|
|
|
198
200
|
this.logger.debug("Producing block", {
|
|
199
201
|
...debugLogCtx,
|
|
@@ -202,9 +204,35 @@ export class BlockProposingService {
|
|
|
202
204
|
payloadLocal,
|
|
203
205
|
builderSelection,
|
|
204
206
|
builderBoostFactor,
|
|
207
|
+
builderMinBid,
|
|
208
|
+
builderUrls: builderEntries.map((entry) => entry.url).join(","),
|
|
205
209
|
});
|
|
206
210
|
this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot));
|
|
207
211
|
|
|
212
|
+
// One entry per resolved builder, authenticated by a request auth signed over the entry's
|
|
213
|
+
// auth data. The auth is usually pre-signed when preferences are submitted ahead of the
|
|
214
|
+
// proposal. An entry whose auth cannot be signed is skipped so it never fails the proposal.
|
|
215
|
+
const builders: routes.validator.BuilderEntry[] = (
|
|
216
|
+
await Promise.all(
|
|
217
|
+
builderEntries.map(async (entry) => {
|
|
218
|
+
try {
|
|
219
|
+
const auth = await this.validatorStore.getBuilderRequestAuth(pubkey, entry.authData, slot, slot);
|
|
220
|
+
return {
|
|
221
|
+
url: new TextEncoder().encode(entry.url),
|
|
222
|
+
auth,
|
|
223
|
+
builderPubkeys: entry.builderPubkeys,
|
|
224
|
+
maxExecutionPayment: entry.maxExecutionPayment,
|
|
225
|
+
minBid: entry.minBid,
|
|
226
|
+
builderBoostFactor: entry.builderBoostFactor,
|
|
227
|
+
};
|
|
228
|
+
} catch (e) {
|
|
229
|
+
this.logger.warn("Failed to sign builder request auth", {...logCtx, builderUrl: entry.url}, e as Error);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
)
|
|
234
|
+
).filter((entry) => entry !== null);
|
|
235
|
+
|
|
208
236
|
// Step 1: Produce beacon block with execution payload bid
|
|
209
237
|
const blockRes = await this.api.validator
|
|
210
238
|
.produceBlockV4({
|
|
@@ -214,7 +242,11 @@ export class BlockProposingService {
|
|
|
214
242
|
feeRecipient,
|
|
215
243
|
strictFeeRecipientCheck,
|
|
216
244
|
includePayload: !payloadLocal,
|
|
217
|
-
|
|
245
|
+
builderConfig: {
|
|
246
|
+
minBid: builderMinBid,
|
|
247
|
+
builderBoostFactor,
|
|
248
|
+
builders,
|
|
249
|
+
},
|
|
218
250
|
})
|
|
219
251
|
.catch((e: Error) => {
|
|
220
252
|
this.metrics?.blockProposingErrors.inc({error: "produce"});
|
|
@@ -250,6 +282,8 @@ export class BlockProposingService {
|
|
|
250
282
|
.publishBlockV2({
|
|
251
283
|
signedBlockContents: {signedBlock},
|
|
252
284
|
broadcastValidation,
|
|
285
|
+
// Echo the winning builder url so any beacon node can forward the block to the builder
|
|
286
|
+
builderUrl: blockMeta.builderUrl,
|
|
253
287
|
})
|
|
254
288
|
.catch((e: Error) => {
|
|
255
289
|
this.metrics?.blockProposingErrors.inc({error: "publish"});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {ApiClient, routes} from "@lodestar/api";
|
|
2
|
+
import {ChainForkConfig} from "@lodestar/config";
|
|
3
|
+
import {SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
|
|
4
|
+
import {IClock, computeEpochAtSlot} from "@lodestar/state-transition";
|
|
5
|
+
import {Epoch, RootHex, Slot} from "@lodestar/types";
|
|
6
|
+
import {toPubkeyHex} from "@lodestar/utils";
|
|
7
|
+
import {Metrics} from "../metrics.js";
|
|
8
|
+
import {LoggerVc} from "../util/index.js";
|
|
9
|
+
import {BlockDutiesService} from "./blockDuties.js";
|
|
10
|
+
import {ValidatorStore} from "./validatorStore.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Submit a proposer's builder preferences this many slots before the proposal slot.
|
|
14
|
+
* Same pacing as `ProposerPreferencesService`, builders must have the preferences
|
|
15
|
+
* before the bid request arrives at proposal time.
|
|
16
|
+
*/
|
|
17
|
+
const SUBMIT_BEFORE_PROPOSAL_SLOTS = Math.floor(SLOTS_PER_EPOCH / 4);
|
|
18
|
+
|
|
19
|
+
/** Per-epoch tracking of preferences already submitted under the current dependent_root. */
|
|
20
|
+
type SubmittedAtEpoch = {dependentRoot: RootHex; slots: Set<Slot>};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Signs and submits builder preferences for any local validator that will propose within the
|
|
24
|
+
* next `SUBMIT_BEFORE_PROPOSAL_SLOTS` and has external builders configured. Signing the request
|
|
25
|
+
* auths here also pre-fills the auth cache used at proposal time.
|
|
26
|
+
*
|
|
27
|
+
* The beacon node submits each entry to the builder at the entry's url. Re-submits automatically
|
|
28
|
+
* when the proposer dependent root for an epoch shifts (e.g. after a reorg) since the proposer
|
|
29
|
+
* for a slot may have changed.
|
|
30
|
+
*/
|
|
31
|
+
export class BuilderPreferencesService {
|
|
32
|
+
private readonly submitted = new Map<Epoch, SubmittedAtEpoch>();
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly config: ChainForkConfig,
|
|
36
|
+
private readonly logger: LoggerVc,
|
|
37
|
+
private readonly api: ApiClient,
|
|
38
|
+
clock: IClock,
|
|
39
|
+
private readonly validatorStore: ValidatorStore,
|
|
40
|
+
private readonly blockDutiesService: BlockDutiesService,
|
|
41
|
+
_metrics: Metrics | null
|
|
42
|
+
) {
|
|
43
|
+
clock.runEverySlot(this.runBuilderPreferencesTask);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private runBuilderPreferencesTask = async (slot: Slot): Promise<void> => {
|
|
47
|
+
// Start running once the submission window (`slot + SUBMIT_BEFORE_PROPOSAL_SLOTS`) reaches
|
|
48
|
+
// Gloas, i.e. already in the epoch before the fork. This allows builders to prepare and
|
|
49
|
+
// submit bids for the first Gloas slots.
|
|
50
|
+
if (!isForkPostGloas(this.config.getForkName(slot + SUBMIT_BEFORE_PROPOSAL_SLOTS))) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const currentEpoch = computeEpochAtSlot(slot);
|
|
55
|
+
const entries: routes.validator.BuilderPreferencesEntry[] = [];
|
|
56
|
+
// Track which `(submission, slot)` pairs are pending an API submission so we can mark
|
|
57
|
+
// them only after the network call succeeds. Marking before would silently drop a
|
|
58
|
+
// preference on transient API failure (no retry until dependent_root shifts).
|
|
59
|
+
const pending: {submission: SubmittedAtEpoch; slot: Slot}[] = [];
|
|
60
|
+
|
|
61
|
+
for (const epoch of [currentEpoch, currentEpoch + 1]) {
|
|
62
|
+
const dutiesAtEpoch = this.blockDutiesService.getProposersAtEpoch(epoch);
|
|
63
|
+
if (!dutiesAtEpoch) continue;
|
|
64
|
+
|
|
65
|
+
// Reset submission tracking if the dependent root for this epoch has shifted
|
|
66
|
+
// (e.g. due to a reorg). The proposer for a slot may have changed.
|
|
67
|
+
let submission = this.submitted.get(epoch);
|
|
68
|
+
if (submission === undefined || submission.dependentRoot !== dutiesAtEpoch.dependentRoot) {
|
|
69
|
+
if (submission !== undefined) {
|
|
70
|
+
this.logger.info("Proposer-shuffling dependent root shifted; resubmitting builder preferences", {
|
|
71
|
+
epoch,
|
|
72
|
+
priorDependentRoot: submission.dependentRoot,
|
|
73
|
+
dependentRoot: dutiesAtEpoch.dependentRoot,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
submission = {dependentRoot: dutiesAtEpoch.dependentRoot, slots: new Set()};
|
|
77
|
+
this.submitted.set(epoch, submission);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const duty of dutiesAtEpoch.data) {
|
|
81
|
+
if (duty.slot <= slot) continue;
|
|
82
|
+
if (duty.slot > slot + SUBMIT_BEFORE_PROPOSAL_SLOTS) continue;
|
|
83
|
+
if (!isForkPostGloas(this.config.getForkName(duty.slot))) continue;
|
|
84
|
+
if (submission.slots.has(duty.slot)) continue;
|
|
85
|
+
|
|
86
|
+
const pubkeyHex = toPubkeyHex(duty.pubkey);
|
|
87
|
+
const {selection} = this.validatorStore.getBuilderSelectionParams(pubkeyHex, duty.slot);
|
|
88
|
+
if (selection === routes.validator.BuilderSelection.ExecutionOnly) continue;
|
|
89
|
+
|
|
90
|
+
const builderEntries = this.validatorStore.getResolvedBuilderEntries(pubkeyHex);
|
|
91
|
+
if (builderEntries.length === 0) continue;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Collect entries per duty and only add them to the batch if signing
|
|
95
|
+
// succeeded for all builders, else the duty is retried on the next tick
|
|
96
|
+
const dutyEntries: routes.validator.BuilderPreferencesEntry[] = [];
|
|
97
|
+
for (const entry of builderEntries) {
|
|
98
|
+
const auth = await this.validatorStore.getBuilderRequestAuth(duty.pubkey, entry.authData, duty.slot, slot);
|
|
99
|
+
dutyEntries.push({
|
|
100
|
+
proposerPubkey: duty.pubkey,
|
|
101
|
+
url: new TextEncoder().encode(entry.url),
|
|
102
|
+
auth,
|
|
103
|
+
maxExecutionPayment: entry.maxExecutionPayment,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
entries.push(...dutyEntries);
|
|
107
|
+
pending.push({submission, slot: duty.slot});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
this.logger.error(
|
|
110
|
+
"Error signing builder preferences",
|
|
111
|
+
{slot: duty.slot, validatorIndex: duty.validatorIndex},
|
|
112
|
+
e as Error
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Prune tracking for past epochs
|
|
119
|
+
for (const epoch of this.submitted.keys()) {
|
|
120
|
+
if (epoch < currentEpoch) {
|
|
121
|
+
this.submitted.delete(epoch);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (entries.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
(await this.api.validator.submitBuilderPreferences({builderPreferences: entries})).assertOk();
|
|
131
|
+
// Only mark as submitted after the API call succeeds; a thrown error, including per-entry
|
|
132
|
+
// failures reported by index, leaves all slots eligible for retry on the next tick.
|
|
133
|
+
// Re-submitting preferences a builder already accepted is harmless.
|
|
134
|
+
for (const {submission, slot: submittedSlot} of pending) {
|
|
135
|
+
submission.slots.add(submittedSlot);
|
|
136
|
+
}
|
|
137
|
+
this.logger.debug("Submitted builder preferences", {count: entries.length});
|
|
138
|
+
} catch (e) {
|
|
139
|
+
this.logger.error("Error submitting builder preferences", {count: entries.length}, e as Error);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -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
|
|
@@ -185,6 +210,9 @@ export class ValidatorStore {
|
|
|
185
210
|
gasLimit: defaultConfig.builder?.gasLimit,
|
|
186
211
|
selection: defaultConfig.builder?.selection,
|
|
187
212
|
boostFactor: builderBoostFactor,
|
|
213
|
+
minBid: defaultConfig.builder?.minBid ?? defaultOptions.builderMinBid,
|
|
214
|
+
maxExecutionPayment: defaultConfig.builder?.maxExecutionPayment ?? defaultOptions.builderMaxExecutionPayment,
|
|
215
|
+
builders: defaultConfig.builder?.builders,
|
|
188
216
|
},
|
|
189
217
|
};
|
|
190
218
|
|
|
@@ -287,11 +315,22 @@ export class ValidatorStore {
|
|
|
287
315
|
// whether they are received over p2p or through a builder API. Pre-gloas there is no
|
|
288
316
|
// in-protocol builder, so the default remains local-only (executiononly).
|
|
289
317
|
const isPostGloas = slot !== undefined && this.config.getForkSeq(slot) >= ForkSeq.gloas;
|
|
318
|
+
return this.resolveBuilderSelectionParams(pubkeyHex, isPostGloas);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private resolveBuilderSelectionParams(
|
|
322
|
+
pubkeyHex: PubkeyHex,
|
|
323
|
+
isPostGloas: boolean
|
|
324
|
+
): {selection: routes.validator.BuilderSelection; boostFactor: bigint} {
|
|
325
|
+
const validatorBuilder = this.validators.get(pubkeyHex)?.builder;
|
|
290
326
|
const defaultSelection = isPostGloas ? defaultOptions.builderAliasSelection : defaultOptions.builderSelection;
|
|
291
|
-
let selection =
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
327
|
+
let selection = validatorBuilder?.selection ?? this.defaultProposerConfig.builder.selection ?? defaultSelection;
|
|
328
|
+
|
|
329
|
+
// The standard per-key builder config directly controls the post-Gloas boost. It takes
|
|
330
|
+
// precedence over the legacy selection aliases when explicitly configured.
|
|
331
|
+
if (isPostGloas && validatorBuilder?.boostFactor !== undefined) {
|
|
332
|
+
return {selection: routes.validator.BuilderSelection.MaxProfit, boostFactor: validatorBuilder.boostFactor};
|
|
333
|
+
}
|
|
295
334
|
|
|
296
335
|
// Post-Gloas block production uses standard builder boost factor. Need to normalize the
|
|
297
336
|
// gloas-deprecated "builderonly" and "executiononly" to the gloas fallback "builderalways"
|
|
@@ -313,8 +352,7 @@ export class ValidatorStore {
|
|
|
313
352
|
break;
|
|
314
353
|
|
|
315
354
|
case routes.validator.BuilderSelection.MaxProfit:
|
|
316
|
-
boostFactor =
|
|
317
|
-
this.validators.get(pubkeyHex)?.builder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor;
|
|
355
|
+
boostFactor = validatorBuilder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor;
|
|
318
356
|
break;
|
|
319
357
|
|
|
320
358
|
case routes.validator.BuilderSelection.BuilderAlways:
|
|
@@ -409,6 +447,126 @@ export class ValidatorStore {
|
|
|
409
447
|
delete validatorData.builder?.boostFactor;
|
|
410
448
|
}
|
|
411
449
|
|
|
450
|
+
getBuilderMinBid(pubkeyHex: PubkeyHex): bigint {
|
|
451
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
452
|
+
if (validatorData === undefined) {
|
|
453
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
454
|
+
}
|
|
455
|
+
return validatorData?.builder?.minBid ?? this.defaultProposerConfig.builder.minBid;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
getBuilderMaxExecutionPayment(pubkeyHex: PubkeyHex): bigint {
|
|
459
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
460
|
+
if (validatorData === undefined) {
|
|
461
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
462
|
+
}
|
|
463
|
+
return validatorData?.builder?.maxExecutionPayment ?? this.defaultProposerConfig.builder.maxExecutionPayment;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Resolve the builder entries for this key. Per-key entries replace the validator client's
|
|
468
|
+
* builders. A value omitted on an entry takes this key's default, then the validator
|
|
469
|
+
* client's configuration, while omitted auth data is derived from the entry url instead.
|
|
470
|
+
*/
|
|
471
|
+
getResolvedBuilderEntries(pubkeyHex: PubkeyHex, boostFactor?: bigint): ResolvedBuilderEntry[] {
|
|
472
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
473
|
+
if (validatorData === undefined) {
|
|
474
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const keyMinBid = validatorData.builder?.minBid ?? this.defaultProposerConfig.builder.minBid;
|
|
478
|
+
const keyBoostFactor =
|
|
479
|
+
boostFactor ?? validatorData.builder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor;
|
|
480
|
+
const keyMaxExecutionPayment =
|
|
481
|
+
validatorData.builder?.maxExecutionPayment ?? this.defaultProposerConfig.builder.maxExecutionPayment;
|
|
482
|
+
|
|
483
|
+
// The key's defaults apply to the validator client's own builders all the same
|
|
484
|
+
const builders = validatorData.builder?.builders ?? this.defaultProposerConfig.builder.builders ?? [];
|
|
485
|
+
return builders.map((entry) => ({
|
|
486
|
+
url: entry.url,
|
|
487
|
+
authData: entry.authData !== undefined ? fromHex(entry.authData) : new TextEncoder().encode(entry.url),
|
|
488
|
+
builderPubkeys: (entry.builderPubkeys ?? []).map(fromHex),
|
|
489
|
+
maxExecutionPayment: entry.maxExecutionPayment ?? keyMaxExecutionPayment,
|
|
490
|
+
minBid: entry.minBid ?? keyMinBid,
|
|
491
|
+
builderBoostFactor: entry.builderBoostFactor ?? keyBoostFactor,
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Return the builder configuration in effect for this key, with omitted values resolved */
|
|
496
|
+
getBuilderConfig(pubkeyHex: PubkeyHex): BuilderConfigData {
|
|
497
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
498
|
+
if (validatorData === undefined) {
|
|
499
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
500
|
+
}
|
|
501
|
+
const {boostFactor} = this.resolveBuilderSelectionParams(pubkeyHex, true);
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
minBid: validatorData.builder?.minBid ?? this.defaultProposerConfig.builder.minBid,
|
|
505
|
+
builderBoostFactor: boostFactor,
|
|
506
|
+
builders: this.getResolvedBuilderEntries(pubkeyHex, boostFactor).map((entry) => ({
|
|
507
|
+
url: entry.url,
|
|
508
|
+
authData: toHex(entry.authData),
|
|
509
|
+
builderPubkeys: entry.builderPubkeys.map(toPubkeyHex),
|
|
510
|
+
maxExecutionPayment: entry.maxExecutionPayment,
|
|
511
|
+
minBid: entry.minBid,
|
|
512
|
+
builderBoostFactor: entry.builderBoostFactor,
|
|
513
|
+
})),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Set the builder configuration for this key, replacing any stored configuration in full */
|
|
518
|
+
setBuilderConfig(pubkeyHex: PubkeyHex, config: BuilderConfigData): void {
|
|
519
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
520
|
+
if (validatorData === undefined) {
|
|
521
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
for (const value of [config.minBid, config.builderBoostFactor]) {
|
|
525
|
+
if (value !== undefined && value > MAX_BUILDER_BOOST_FACTOR) {
|
|
526
|
+
throw Error(`Invalid builder config value=${value} exceeds uint64`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// No two entries may share both their url and their auth data, an omitted auth data is
|
|
531
|
+
// compared as the value derived from the entry url
|
|
532
|
+
const seenEntries = new Set<string>();
|
|
533
|
+
for (const entry of config.builders ?? []) {
|
|
534
|
+
if (!isValidAsciiHttpUrl(entry.url)) {
|
|
535
|
+
throw Error(`Invalid builder url: ${entry.url}`);
|
|
536
|
+
}
|
|
537
|
+
const authData =
|
|
538
|
+
entry.authData !== undefined ? toHex(fromHex(entry.authData)) : toHex(new TextEncoder().encode(entry.url));
|
|
539
|
+
const entryKey = `${entry.url}|${authData}`;
|
|
540
|
+
if (seenEntries.has(entryKey)) {
|
|
541
|
+
throw Error(`Duplicate builder entry url=${entry.url} authData=${authData}`);
|
|
542
|
+
}
|
|
543
|
+
seenEntries.add(entryKey);
|
|
544
|
+
for (const value of [entry.maxExecutionPayment, entry.minBid, entry.builderBoostFactor]) {
|
|
545
|
+
if (value !== undefined && value > MAX_BUILDER_BOOST_FACTOR) {
|
|
546
|
+
throw Error(`Invalid builder entry value=${value} exceeds uint64`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
validatorData.builder = {
|
|
552
|
+
...validatorData.builder,
|
|
553
|
+
minBid: config.minBid,
|
|
554
|
+
boostFactor: config.builderBoostFactor,
|
|
555
|
+
builders: config.builders,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Remove the builder configuration for this key, and revert to the validator client configuration */
|
|
560
|
+
deleteBuilderConfig(pubkeyHex: PubkeyHex): void {
|
|
561
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
562
|
+
if (validatorData === undefined) {
|
|
563
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
564
|
+
}
|
|
565
|
+
delete validatorData.builder?.minBid;
|
|
566
|
+
delete validatorData.builder?.boostFactor;
|
|
567
|
+
delete validatorData.builder?.builders;
|
|
568
|
+
}
|
|
569
|
+
|
|
412
570
|
/** Return true if `index` is active part of this validator client */
|
|
413
571
|
hasValidatorIndex(index: ValidatorIndex): boolean {
|
|
414
572
|
return this.indicesService.index2pubkey.has(index);
|
|
@@ -430,7 +588,10 @@ export class ValidatorStore {
|
|
|
430
588
|
feeRecipient !== undefined ||
|
|
431
589
|
builder?.gasLimit !== undefined ||
|
|
432
590
|
builder?.selection !== undefined ||
|
|
433
|
-
builder?.boostFactor !== undefined
|
|
591
|
+
builder?.boostFactor !== undefined ||
|
|
592
|
+
builder?.minBid !== undefined ||
|
|
593
|
+
builder?.maxExecutionPayment !== undefined ||
|
|
594
|
+
builder?.builders !== undefined
|
|
434
595
|
) {
|
|
435
596
|
proposerConfig = {graffiti, strictFeeRecipientCheck, feeRecipient, builder};
|
|
436
597
|
}
|
|
@@ -879,6 +1040,71 @@ export class ValidatorStore {
|
|
|
879
1040
|
};
|
|
880
1041
|
}
|
|
881
1042
|
|
|
1043
|
+
async signBuilderRequestAuth(
|
|
1044
|
+
pubkeyMaybeHex: BLSPubkeyMaybeHex,
|
|
1045
|
+
data: Uint8Array,
|
|
1046
|
+
proposalSlot: Slot
|
|
1047
|
+
): Promise<gloas.SignedBuilderRequestAuth> {
|
|
1048
|
+
if (data.length === 0 || data.length > MAX_BUILDER_AUTH_DATA_SIZE) {
|
|
1049
|
+
throw Error(
|
|
1050
|
+
`Invalid builder request auth data length=${data.length}, must be within 1 and ${MAX_BUILDER_AUTH_DATA_SIZE} bytes`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const message: gloas.BuilderRequestAuth = {data, slot: proposalSlot};
|
|
1055
|
+
|
|
1056
|
+
const signingSlot = 0;
|
|
1057
|
+
const domain = computeDomain(DOMAIN_BUILDER_REQUEST_AUTH, this.config.GENESIS_FORK_VERSION, ZERO_HASH);
|
|
1058
|
+
const signingRoot = computeSigningRoot(ssz.gloas.BuilderRequestAuth, message, domain);
|
|
1059
|
+
|
|
1060
|
+
const signableMessage: SignableMessage = {
|
|
1061
|
+
type: SignableMessageType.BUILDER_REQUEST_AUTH,
|
|
1062
|
+
data: message,
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
return {
|
|
1066
|
+
message,
|
|
1067
|
+
signature: await this.getSignature(pubkeyMaybeHex, signingRoot, signingSlot, signableMessage),
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Return a pre-signed builder request auth for the auth data and proposal slot, or sign and cache a new
|
|
1073
|
+
* one. Signing happens off the block proposal hot path when preferences are submitted ahead of
|
|
1074
|
+
* time, cached auths are then used just-in-time when requesting bids at proposal time.
|
|
1075
|
+
*/
|
|
1076
|
+
async getBuilderRequestAuth(
|
|
1077
|
+
pubkeyMaybeHex: BLSPubkeyMaybeHex,
|
|
1078
|
+
data: Uint8Array,
|
|
1079
|
+
proposalSlot: Slot,
|
|
1080
|
+
currentSlot: Slot
|
|
1081
|
+
): Promise<gloas.SignedBuilderRequestAuth> {
|
|
1082
|
+
const pubkeyHex = typeof pubkeyMaybeHex === "string" ? pubkeyMaybeHex : toPubkeyHex(pubkeyMaybeHex);
|
|
1083
|
+
const authKey = `${proposalSlot}-${toHex(data)}`;
|
|
1084
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
1085
|
+
const cached = validatorData?.builderRequestAuths?.get(authKey);
|
|
1086
|
+
if (cached !== undefined) {
|
|
1087
|
+
return cached;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const signedRequestAuth = await this.signBuilderRequestAuth(pubkeyMaybeHex, data, proposalSlot);
|
|
1091
|
+
|
|
1092
|
+
if (validatorData !== undefined) {
|
|
1093
|
+
const builderRequestAuths =
|
|
1094
|
+
validatorData.builderRequestAuths ?? new Map<string, gloas.SignedBuilderRequestAuth>();
|
|
1095
|
+
// Prune auths for proposal slots that are already in the past
|
|
1096
|
+
for (const key of builderRequestAuths.keys()) {
|
|
1097
|
+
if (Number(key.slice(0, key.indexOf("-"))) < currentSlot) {
|
|
1098
|
+
builderRequestAuths.delete(key);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
builderRequestAuths.set(authKey, signedRequestAuth);
|
|
1102
|
+
validatorData.builderRequestAuths = builderRequestAuths;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return signedRequestAuth;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
882
1108
|
async getValidatorRegistration(
|
|
883
1109
|
pubkeyMaybeHex: BLSPubkeyMaybeHex,
|
|
884
1110
|
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
|
@@ -16,6 +16,7 @@ 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";
|
|
@@ -313,6 +314,7 @@ export class Validator {
|
|
|
313
314
|
);
|
|
314
315
|
|
|
315
316
|
new ProposerPreferencesService(config, loggerVc, api, clock, validatorStore, blockDutiesService, metrics);
|
|
317
|
+
new BuilderPreferencesService(config, loggerVc, api, clock, validatorStore, blockDutiesService, metrics);
|
|
316
318
|
|
|
317
319
|
return new Validator({
|
|
318
320
|
opts,
|