@lodestar/validator 1.50.0-dev.456b75a81e → 1.50.0-dev.4b00f7bf39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/lib/services/attestation.d.ts.map +1 -1
  2. package/lib/services/attestation.js +1 -2
  3. package/lib/services/attestation.js.map +1 -1
  4. package/lib/services/block.d.ts.map +1 -1
  5. package/lib/services/block.js +15 -4
  6. package/lib/services/block.js.map +1 -1
  7. package/lib/services/blockDuties.d.ts.map +1 -1
  8. package/lib/services/blockDuties.js +3 -1
  9. package/lib/services/blockDuties.js.map +1 -1
  10. package/lib/services/prepareBeaconProposer.d.ts +2 -1
  11. package/lib/services/prepareBeaconProposer.d.ts.map +1 -1
  12. package/lib/services/prepareBeaconProposer.js +5 -1
  13. package/lib/services/prepareBeaconProposer.js.map +1 -1
  14. package/lib/services/proposalPreferences.d.ts +47 -0
  15. package/lib/services/proposalPreferences.d.ts.map +1 -0
  16. package/lib/services/proposalPreferences.js +260 -0
  17. package/lib/services/proposalPreferences.js.map +1 -0
  18. package/lib/services/ptc.d.ts.map +1 -1
  19. package/lib/services/ptc.js +3 -2
  20. package/lib/services/ptc.js.map +1 -1
  21. package/lib/services/syncCommittee.js +1 -1
  22. package/lib/services/syncCommittee.js.map +1 -1
  23. package/lib/services/validatorStore.d.ts.map +1 -1
  24. package/lib/services/validatorStore.js +4 -9
  25. package/lib/services/validatorStore.js.map +1 -1
  26. package/lib/util/externalSignerClient.d.ts +1 -2
  27. package/lib/util/externalSignerClient.d.ts.map +1 -1
  28. package/lib/util/externalSignerClient.js.map +1 -1
  29. package/lib/validator.d.ts.map +1 -1
  30. package/lib/validator.js +2 -4
  31. package/lib/validator.js.map +1 -1
  32. package/package.json +13 -13
  33. package/src/services/attestation.ts +1 -2
  34. package/src/services/block.ts +24 -7
  35. package/src/services/blockDuties.ts +3 -1
  36. package/src/services/prepareBeaconProposer.ts +5 -1
  37. package/src/services/proposalPreferences.ts +308 -0
  38. package/src/services/ptc.ts +6 -2
  39. package/src/services/syncCommittee.ts +1 -1
  40. package/src/services/validatorStore.ts +4 -8
  41. package/src/util/externalSignerClient.ts +2 -2
  42. package/src/validator.ts +11 -4
  43. package/lib/services/builderPreferences.d.ts +0 -27
  44. package/lib/services/builderPreferences.d.ts.map +0 -1
  45. package/lib/services/builderPreferences.js +0 -127
  46. package/lib/services/builderPreferences.js.map +0 -1
  47. package/lib/services/proposerPreferences.d.ts +0 -31
  48. package/lib/services/proposerPreferences.d.ts.map +0 -1
  49. package/lib/services/proposerPreferences.js +0 -130
  50. package/lib/services/proposerPreferences.js.map +0 -1
  51. package/src/services/builderPreferences.ts +0 -142
  52. package/src/services/proposerPreferences.ts +0 -154
@@ -1,6 +1,6 @@
1
1
  import {ApiClient, routes} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
- import {BUILDER_INDEX_SELF_BUILD, ForkPostGloas, isForkPostGloas} from "@lodestar/params";
3
+ import {BUILDER_INDEX_SELF_BUILD, ForkPostGloas, ForkPreGloas, isForkPostGloas} from "@lodestar/params";
4
4
  import {IClock} from "@lodestar/state-transition";
5
5
  import {
6
6
  BLSPubkey,
@@ -9,6 +9,7 @@ import {
9
9
  BlindedBeaconBlock,
10
10
  BlockContents,
11
11
  ProducedBlockSource,
12
+ SignedBeaconBlock,
12
13
  SignedBlindedBeaconBlock,
13
14
  SignedBlockContents,
14
15
  Slot,
@@ -42,6 +43,10 @@ type BlockProposalOpts = {
42
43
  blindedLocal: boolean;
43
44
  payloadLocal: boolean;
44
45
  };
46
+
47
+ /** Excludes all p2p bids, builder API bids are excluded by not sending any builder entries */
48
+ const EXECUTION_ONLY_MIN_BID = 2n ** 64n - 1n;
49
+
45
50
  /**
46
51
  * Service that sets up and handles validator block proposal duties.
47
52
  */
@@ -144,7 +149,9 @@ export class BlockProposingService {
144
149
  const block = blockContentsWrapper.executionPayloadBlinded
145
150
  ? blockContentsWrapper.block
146
151
  : blockContentsWrapper.blockContents.block;
147
- const signedBlock = await this.validatorStore.signBlock(pubkey, block, slot, this.logger);
152
+ const signedBlock = (await this.validatorStore.signBlock(pubkey, block, slot, this.logger)) as
153
+ | SignedBeaconBlock<ForkPreGloas>
154
+ | SignedBlindedBeaconBlock;
148
155
 
149
156
  const {broadcastValidation} = this.opts;
150
157
  const publishOpts = {broadcastValidation};
@@ -194,8 +201,12 @@ export class BlockProposingService {
194
201
  const {broadcastValidation, payloadLocal} = this.opts;
195
202
  const {selection: builderSelection, boostFactor: builderBoostFactor} =
196
203
  this.validatorStore.getBuilderSelectionParams(pubkeyHex, slot);
197
- const builderMinBid = this.validatorStore.getBuilderMinBid(pubkeyHex);
198
- const builderEntries = this.validatorStore.getResolvedBuilderEntries(pubkeyHex, builderBoostFactor);
204
+ // `executiononly` never uses a builder bid, neither from builder APIs nor received over p2p
205
+ const isExecutionOnly = builderSelection === routes.validator.BuilderSelection.ExecutionOnly;
206
+ const builderMinBid = isExecutionOnly ? EXECUTION_ONLY_MIN_BID : this.validatorStore.getBuilderMinBid(pubkeyHex);
207
+ const builderEntries = isExecutionOnly
208
+ ? []
209
+ : this.validatorStore.getResolvedBuilderEntries(pubkeyHex, builderBoostFactor);
199
210
 
200
211
  this.logger.debug("Producing block", {
201
212
  ...debugLogCtx,
@@ -258,6 +269,14 @@ export class BlockProposingService {
258
269
  const block = executionPayloadIncluded
259
270
  ? (blockOrContents as BlockContents<ForkPostGloas>).block
260
271
  : (blockOrContents as BeaconBlock<ForkPostGloas>);
272
+ const {builderIndex} = block.body.signedExecutionPayloadBid.message;
273
+ const isSelfBuild = builderIndex === BUILDER_INDEX_SELF_BUILD;
274
+ if (isExecutionOnly && !isSelfBuild) {
275
+ this.metrics?.blockProposingErrors.inc({error: "produce"});
276
+ throw Error(
277
+ `Block not produced as per desired builderSelection=${builderSelection} builderIndex=${builderIndex}`
278
+ );
279
+ }
261
280
  const beaconBlockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block);
262
281
  const blockRootHex = toRootHex(beaconBlockRoot);
263
282
 
@@ -280,7 +299,7 @@ export class BlockProposingService {
280
299
  (
281
300
  await this.api.beacon
282
301
  .publishBlockV2({
283
- signedBlockContents: {signedBlock},
302
+ signedBlockContents: {signedBlock} as SignedBlockContents<ForkPostGloas>,
284
303
  broadcastValidation,
285
304
  // Echo the winning builder url so any beacon node can forward the block to the builder
286
305
  builderUrl: blockMeta.builderUrl,
@@ -303,8 +322,6 @@ export class BlockProposingService {
303
322
  this.metrics?.proposerStepCallPublishBlock.observe(this.clock.secFromSlot(slot));
304
323
  this.metrics?.blocksPublished.inc();
305
324
 
306
- const isSelfBuild = block.body.signedExecutionPayloadBid.message.builderIndex === BUILDER_INDEX_SELF_BUILD;
307
-
308
325
  if (isSelfBuild) {
309
326
  // Self-build: proposer is responsible for building and publishing the execution payload envelope
310
327
  const flow = executionPayloadIncluded ? "stateless" : "stateful";
@@ -248,8 +248,10 @@ export class BlockDutiesService {
248
248
  signal: AbortSignal
249
249
  ): Promise<void> {
250
250
  const nextSlot = currentSlot + 1;
251
+ const currentFork = this.config.getForkName(currentSlot);
251
252
  const lookAheadMs =
252
- this.config.SLOT_DURATION_MS - this.config.getSlotComponentDurationMs(BLOCK_DUTIES_LOOKAHEAD_BPS);
253
+ this.config.getSlotDurationMs(currentFork) -
254
+ this.config.getSlotComponentDurationMs(currentFork, BLOCK_DUTIES_LOOKAHEAD_BPS);
253
255
  await sleep(this.clock.msToSlot(nextSlot) - lookAheadMs, signal);
254
256
  this.logger.debug("Polling proposers for the next epoch", {nextEpoch, currentSlot});
255
257
  await this.pollBeaconProposers(nextEpoch);
@@ -14,7 +14,8 @@ const REGISTRATION_CHUNK_SIZE = 512;
14
14
  * the engine for building execution payload with transactions.
15
15
  *
16
16
  * This needs to be done every epoch because the BN will cache it at most for
17
- * two epochs.
17
+ * two epochs. From gloas the beacon node takes the fee recipient from the signed
18
+ * proposer preferences and the validators using it from the committee subscriptions.
18
19
  */
19
20
  export function pollPrepareBeaconProposer(
20
21
  config: BeaconConfig,
@@ -28,6 +29,9 @@ export function pollPrepareBeaconProposer(
28
29
  // Before bellatrix we don't need to update this data on bn/builder
29
30
  if (epoch < config.BELLATRIX_FORK_EPOCH - 1) return;
30
31
 
32
+ // Proposer data is pre-gloas only, replaced by proposer preferences and committee subscriptions
33
+ if (config.getForkSeq(epoch * SLOTS_PER_EPOCH) >= ForkSeq.gloas) return;
34
+
31
35
  // prepareBeaconProposer is not as time sensitive as attesting.
32
36
  // Poll indices first, then call api.validator.prepareBeaconProposer once
33
37
  await validatorStore.pollValidatorIndices().catch((e: Error) => {
@@ -0,0 +1,308 @@
1
+ import {ApiClient, ApiError, 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, gloas} from "@lodestar/types";
6
+ import {fromHex, 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 {SyncingStatusTracker} from "./syncingStatusTracker.js";
11
+ import {ValidatorStore} from "./validatorStore.js";
12
+
13
+ /**
14
+ * Submit the preferences of a proposal this many slots before the proposal slot.
15
+ *
16
+ * Earlier submission means more reorg-triggered resubmits (and gossip flood); later
17
+ * submission risks missing the bid-auction window for this proposal slot. The bid for
18
+ * slot S typically arrives at slot S-1, so we want preferences propagated to the network
19
+ * and consumed by builders before then. SLOTS_PER_EPOCH / 4 (8 slots @ 32 SPE, ~96s @ 12s
20
+ * slots) gives ample margin while bounding redundant resubmits.
21
+ */
22
+ const SUBMIT_BEFORE_PROPOSAL_SLOTS = Math.floor(SLOTS_PER_EPOCH / 4);
23
+
24
+ /** Per-epoch tracking of preferences already submitted under the current dependent_root. */
25
+ type SubmittedAtEpoch = {dependentRoot: RootHex; proposerSlots: Set<Slot>; builderSlots: Set<Slot>};
26
+ type PendingSubmission = {submission: SubmittedAtEpoch; slot: Slot};
27
+ /** A local proposal within the submission window along with the tracking of its epoch */
28
+ type UpcomingProposal = {duty: routes.validator.ProposerDuty; submission: SubmittedAtEpoch};
29
+
30
+ /**
31
+ * Signs and submits the preferences of local proposals within the next `SUBMIT_BEFORE_PROPOSAL_SLOTS`:
32
+ * the `SignedProposerPreferences` the beacon node broadcasts on gossip and the builder preferences
33
+ * it forwards to the builders configured for the validator. Signing the builder request auths here
34
+ * also pre-fills the auth cache used at proposal time.
35
+ *
36
+ * Re-submits automatically when the proposer dependent root for an epoch shifts (e.g. after a reorg)
37
+ * since the proposer for a slot may have changed. Detected by comparing the cached `dependentRoot`
38
+ * reported by `BlockDutiesService` against the one we last submitted under.
39
+ *
40
+ * Proposers should broadcast their preferences before the fork so the proposer preference caches
41
+ * of beacon nodes and builders are warm for the first Gloas slots. We start submitting
42
+ * as soon as a duty's proposal slot is in Gloas, which is up to `SUBMIT_BEFORE_PROPOSAL_SLOTS`
43
+ * before the fork, so only the first few Gloas slots are affected by this pre-fork submission.
44
+ */
45
+ export class ProposalPreferencesService {
46
+ private readonly submitted = new Map<Epoch, SubmittedAtEpoch>();
47
+ private activeScheduledGasLimit: number | undefined;
48
+
49
+ constructor(
50
+ private readonly config: ChainForkConfig,
51
+ private readonly logger: LoggerVc,
52
+ private readonly api: ApiClient,
53
+ clock: IClock,
54
+ private readonly validatorStore: ValidatorStore,
55
+ private readonly blockDutiesService: BlockDutiesService,
56
+ syncingStatusTracker: SyncingStatusTracker,
57
+ _metrics: Metrics | null
58
+ ) {
59
+ clock.runEverySlot(this.runPreferencesTask);
60
+ clock.runEveryEpoch(this.runEveryEpochTask);
61
+ syncingStatusTracker.runOnResynced(this.onResynced);
62
+ }
63
+
64
+ /**
65
+ * A beacon node that was unreachable or syncing may have restarted and lost the preferences it
66
+ * accepted, it keeps them in memory only. Submit everything within the window again, the beacon
67
+ * node ignores preferences it still has.
68
+ */
69
+ private onResynced = async (slot: Slot): Promise<void> => {
70
+ if (this.submitted.size === 0) {
71
+ return;
72
+ }
73
+
74
+ this.logger.verbose("Beacon node resynced; resubmitting preferences", {slot});
75
+ this.submitted.clear();
76
+ await this.runPreferencesTask(slot);
77
+ };
78
+
79
+ private runPreferencesTask = async (slot: Slot): Promise<void> => {
80
+ // Start running once the submission window (`slot + SUBMIT_BEFORE_PROPOSAL_SLOTS`) reaches
81
+ // Gloas, i.e. already in the epoch before the fork. This allows builders to prepare and
82
+ // submit bids for the first Gloas slots.
83
+ if (!isForkPostGloas(this.config.getForkName(slot + SUBMIT_BEFORE_PROPOSAL_SLOTS))) {
84
+ return;
85
+ }
86
+
87
+ const proposals = this.getUpcomingProposals(slot);
88
+ if (proposals.length === 0) {
89
+ return;
90
+ }
91
+
92
+ // Signed and submitted independently, a slow signer for one must not delay the other
93
+ await Promise.all([
94
+ this.submitProposerPreferences(proposals, slot),
95
+ this.submitBuilderPreferences(proposals, slot),
96
+ ]);
97
+ };
98
+
99
+ private runEveryEpochTask = async (epoch: Epoch): Promise<void> => {
100
+ const scheduledGasLimit = this.config.getScheduledGasLimit(epoch);
101
+ if (scheduledGasLimit !== undefined && scheduledGasLimit !== this.activeScheduledGasLimit) {
102
+ const configuredDefaultGasLimit = this.validatorStore.getConfiguredDefaultGasLimit();
103
+ this.logger.info("Gas limit schedule active", {
104
+ epoch,
105
+ recommendedGasLimit: scheduledGasLimit,
106
+ effectiveDefaultGasLimit: configuredDefaultGasLimit ?? scheduledGasLimit,
107
+ defaultGasLimitSource: configuredDefaultGasLimit === undefined ? "schedule" : "operator",
108
+ });
109
+ this.activeScheduledGasLimit = scheduledGasLimit;
110
+ }
111
+
112
+ // Drop tracking for past epochs; only currentEpoch and currentEpoch + 1 are ever processed.
113
+ for (const trackedEpoch of this.submitted.keys()) {
114
+ if (trackedEpoch < epoch) {
115
+ this.submitted.delete(trackedEpoch);
116
+ }
117
+ }
118
+ };
119
+
120
+ /** Resets the tracking of `epoch` if the dependent root shifted, previously submitted preferences are stale. */
121
+ private getSubmissionAtEpoch(epoch: Epoch, dependentRoot: RootHex): SubmittedAtEpoch {
122
+ let submission = this.submitted.get(epoch);
123
+ if (submission === undefined || submission.dependentRoot !== dependentRoot) {
124
+ if (submission !== undefined) {
125
+ this.logger.info("Proposer-shuffling dependent root shifted; resubmitting preferences", {
126
+ epoch,
127
+ priorDependentRoot: submission.dependentRoot,
128
+ dependentRoot,
129
+ });
130
+ }
131
+ submission = {dependentRoot, proposerSlots: new Set(), builderSlots: new Set()};
132
+ this.submitted.set(epoch, submission);
133
+ }
134
+ return submission;
135
+ }
136
+
137
+ private getUpcomingProposals(slot: Slot): UpcomingProposal[] {
138
+ const currentEpoch = computeEpochAtSlot(slot);
139
+ const proposals: UpcomingProposal[] = [];
140
+
141
+ for (const epoch of [currentEpoch, currentEpoch + 1]) {
142
+ const dutiesAtEpoch = this.blockDutiesService.getProposersAtEpoch(epoch);
143
+ if (!dutiesAtEpoch) continue;
144
+
145
+ const submission = this.getSubmissionAtEpoch(epoch, dutiesAtEpoch.dependentRoot);
146
+
147
+ for (const duty of dutiesAtEpoch.data) {
148
+ if (duty.slot <= slot) continue;
149
+ if (duty.slot > slot + SUBMIT_BEFORE_PROPOSAL_SLOTS) continue;
150
+ if (!isForkPostGloas(this.config.getForkName(duty.slot))) continue;
151
+
152
+ proposals.push({duty, submission});
153
+ }
154
+ }
155
+
156
+ return proposals;
157
+ }
158
+
159
+ private async submitProposerPreferences(proposals: UpcomingProposal[], slot: Slot): Promise<void> {
160
+ const signedProposerPreferences: gloas.SignedProposerPreferences[] = [];
161
+ // Track which `(submission, slot)` pairs are pending an API submission so we can mark
162
+ // them only after the network call succeeds. Marking before would silently drop a
163
+ // preference on transient API failure (no retry until dependent_root shifts).
164
+ const pending: PendingSubmission[] = [];
165
+
166
+ for (const {duty, submission} of proposals) {
167
+ if (submission.proposerSlots.has(duty.slot)) continue;
168
+
169
+ try {
170
+ const pubkeyHex = toPubkeyHex(duty.pubkey);
171
+ const signed = await this.validatorStore.signProposerPreferences(
172
+ duty,
173
+ fromHex(submission.dependentRoot),
174
+ this.validatorStore.getFeeRecipient(pubkeyHex),
175
+ this.validatorStore.getGasLimit(pubkeyHex, duty.slot, this.logger),
176
+ slot
177
+ );
178
+ signedProposerPreferences.push(signed);
179
+ pending.push({submission, slot: duty.slot});
180
+ } catch (e) {
181
+ this.logger.error(
182
+ "Error signing proposer preferences",
183
+ {slot: duty.slot, validatorIndex: duty.validatorIndex},
184
+ e as Error
185
+ );
186
+ }
187
+ }
188
+
189
+ if (signedProposerPreferences.length === 0) {
190
+ return;
191
+ }
192
+
193
+ try {
194
+ (await this.api.validator.submitProposerPreferences({signedProposerPreferences})).assertOk();
195
+ // Only mark as submitted after the API call succeeds; a thrown error leaves the
196
+ // slot eligible for retry on the next tick.
197
+ for (const {submission, slot: submittedSlot} of pending) {
198
+ submission.proposerSlots.add(submittedSlot);
199
+ }
200
+ this.logger.debug("Submitted signed proposer preferences", {count: signedProposerPreferences.length});
201
+ } catch (e) {
202
+ const failures = e instanceof ApiError ? e.failures : undefined;
203
+ if (failures === undefined) {
204
+ this.logger.error(
205
+ "Error submitting signed proposer preferences",
206
+ {count: signedProposerPreferences.length},
207
+ e as Error
208
+ );
209
+ return;
210
+ }
211
+
212
+ // The beacon node rejected some of the preferences, reported by index into the batch.
213
+ // Mark the accepted ones, the rejected ones are retried on the next tick.
214
+ const failedIndices = new Set(failures.map(({index}) => index));
215
+ pending.forEach(({submission, slot: submittedSlot}, index) => {
216
+ if (!failedIndices.has(index)) {
217
+ submission.proposerSlots.add(submittedSlot);
218
+ }
219
+ });
220
+ for (const {index, message} of failures) {
221
+ this.logger.error("Error submitting signed proposer preferences", {
222
+ slot: pending[index]?.slot,
223
+ validatorIndex: signedProposerPreferences[index]?.message.validatorIndex,
224
+ message,
225
+ });
226
+ }
227
+ }
228
+ }
229
+
230
+ private async submitBuilderPreferences(proposals: UpcomingProposal[], slot: Slot): Promise<void> {
231
+ const builderPreferences: routes.validator.BuilderPreferencesEntry[] = [];
232
+ const pending: PendingSubmission[] = [];
233
+ // Index into `pending` of each entry in `builderPreferences`, a proposal has one entry per builder
234
+ const pendingIndexByEntry: number[] = [];
235
+
236
+ for (const {duty, submission} of proposals) {
237
+ if (submission.builderSlots.has(duty.slot)) continue;
238
+
239
+ const pubkeyHex = toPubkeyHex(duty.pubkey);
240
+ const {selection} = this.validatorStore.getBuilderSelectionParams(pubkeyHex, duty.slot);
241
+ if (selection === routes.validator.BuilderSelection.ExecutionOnly) continue;
242
+
243
+ const builderEntries = this.validatorStore.getResolvedBuilderEntries(pubkeyHex);
244
+ if (builderEntries.length === 0) continue;
245
+
246
+ try {
247
+ // Collect entries per duty and only add them to the batch if signing
248
+ // succeeded for all builders, else the duty is retried on the next tick
249
+ const dutyEntries: routes.validator.BuilderPreferencesEntry[] = [];
250
+ for (const entry of builderEntries) {
251
+ const auth = await this.validatorStore.getBuilderRequestAuth(duty.pubkey, entry.authData, duty.slot, slot);
252
+ dutyEntries.push({
253
+ proposerPubkey: duty.pubkey,
254
+ url: new TextEncoder().encode(entry.url),
255
+ auth,
256
+ maxExecutionPayment: entry.maxExecutionPayment,
257
+ });
258
+ }
259
+ builderPreferences.push(...dutyEntries);
260
+ pendingIndexByEntry.push(...dutyEntries.map(() => pending.length));
261
+ pending.push({submission, slot: duty.slot});
262
+ } catch (e) {
263
+ this.logger.error(
264
+ "Error signing builder preferences",
265
+ {slot: duty.slot, validatorIndex: duty.validatorIndex},
266
+ e as Error
267
+ );
268
+ }
269
+ }
270
+
271
+ if (builderPreferences.length === 0) {
272
+ return;
273
+ }
274
+
275
+ try {
276
+ (await this.api.validator.submitBuilderPreferences({builderPreferences})).assertOk();
277
+ // Only mark as submitted after the API call succeeds; a thrown error, including per-entry
278
+ // failures reported by index, leaves all slots eligible for retry on the next tick.
279
+ // Re-submitting preferences a builder already accepted is harmless.
280
+ for (const {submission, slot: submittedSlot} of pending) {
281
+ submission.builderSlots.add(submittedSlot);
282
+ }
283
+ this.logger.debug("Submitted builder preferences", {count: builderPreferences.length});
284
+ } catch (e) {
285
+ const failures = e instanceof ApiError ? e.failures : undefined;
286
+ if (failures === undefined) {
287
+ this.logger.warn("Error submitting builder preferences", {count: builderPreferences.length}, e as Error);
288
+ return;
289
+ }
290
+
291
+ // Some entries were not accepted by their builder, reported by index into the batch. Mark the
292
+ // proposals whose entries were all accepted, the others are retried with all their builders.
293
+ const failedPendingIndices = new Set(failures.map(({index}) => pendingIndexByEntry[index]));
294
+ pending.forEach(({submission, slot: submittedSlot}, index) => {
295
+ if (!failedPendingIndices.has(index)) {
296
+ submission.builderSlots.add(submittedSlot);
297
+ }
298
+ });
299
+ for (const {index, message} of failures) {
300
+ this.logger.warn("Error submitting builder preferences", {
301
+ slot: pending[pendingIndexByEntry[index]]?.slot,
302
+ builder: builderPreferences[index] ? new TextDecoder().decode(builderPreferences[index].url) : undefined,
303
+ message,
304
+ });
305
+ }
306
+ }
307
+ }
308
+ }
@@ -61,7 +61,10 @@ export class PtcService {
61
61
  return;
62
62
  }
63
63
 
64
- const payloadAttestationDueMs = this.config.getSlotComponentDurationMs(this.config.PAYLOAD_ATTESTATION_DUE_BPS);
64
+ const payloadAttestationDueMs = this.config.getSlotComponentDurationMs(
65
+ fork,
66
+ this.config.PAYLOAD_ATTESTATION_DUE_BPS
67
+ );
65
68
  // Submit as soon as the canonical head block's payload is available, or at the deadline
66
69
  const payloadAvailable = new AbortController();
67
70
  try {
@@ -149,7 +152,8 @@ export class PtcService {
149
152
 
150
153
  this.metrics?.ptcStepCallPublishPayloadAttestation.observe(
151
154
  this.clock.secFromSlot(slot) -
152
- this.config.getSlotComponentDurationMs(this.config.PAYLOAD_ATTESTATION_DUE_BPS) / 1000
155
+ this.config.getSlotComponentDurationMs(this.config.getForkName(slot), this.config.PAYLOAD_ATTESTATION_DUE_BPS) /
156
+ 1000
153
157
  );
154
158
 
155
159
  if (payloadAttestationMessages.length > 0) {
@@ -174,7 +174,7 @@ export class SyncCommitteeService {
174
174
  // SyncCommitteeSignature gossip validation.
175
175
  const syncMessageDueMs = this.config.getSyncMessageDueMs(fork);
176
176
  const msToCutoffTime = syncMessageDueMs - this.clock.msFromSlot(slot);
177
- const afterBlockDelayMs = 1000 * this.clock.secondsPerSlot * (this.opts?.scAfterBlockDelaySlotFraction ?? 0);
177
+ const afterBlockDelayMs = this.clock.getSlotDurationMs(slot) * (this.opts?.scAfterBlockDelaySlotFraction ?? 0);
178
178
  const toDelayMs = Math.min(msToCutoffTime, afterBlockDelayMs);
179
179
  if (toDelayMs > 0) {
180
180
  await sleep(toDelayMs);
@@ -359,14 +359,10 @@ export class ValidatorStore {
359
359
  }
360
360
 
361
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
- }
362
+ // gloas-deprecated "builderonly" to the gloas fallback "builderalways" equivalent before
363
+ // deriving the boost factor.
364
+ if (isPostGloas && selection === routes.validator.BuilderSelection.BuilderOnly) {
365
+ selection = routes.validator.BuilderSelection.BuilderAlways;
370
366
  }
371
367
 
372
368
  const boostFactor = getBuilderBoostFactor(
@@ -1,6 +1,6 @@
1
1
  import {ContainerType, ValueOf} from "@chainsafe/ssz";
2
2
  import {BeaconConfig} from "@lodestar/config";
3
- import {ForkName, ForkPostGloas, ForkPreBellatrix, ForkSeq, isForkPostDeneb} from "@lodestar/params";
3
+ import {ForkName, ForkPostGloas, ForkSeq, isForkPostDeneb} from "@lodestar/params";
4
4
  import {blindedOrFullBlockToHeader, computeEpochAtSlot} from "@lodestar/state-transition";
5
5
  import {
6
6
  AggregateAndProof,
@@ -78,7 +78,7 @@ export type SignableMessage =
78
78
  | {type: SignableMessageType.AGGREGATE_AND_PROOF; data: phase0.AggregateAndProof}
79
79
  | {type: SignableMessageType.AGGREGATE_AND_PROOF_V2; data: AggregateAndProof}
80
80
  | {type: SignableMessageType.ATTESTATION; data: phase0.AttestationData}
81
- | {type: SignableMessageType.BLOCK_V2; data: BeaconBlock<ForkPreBellatrix> | BlindedBeaconBlock}
81
+ | {type: SignableMessageType.BLOCK_V2; data: BeaconBlock | BlindedBeaconBlock}
82
82
  | {type: SignableMessageType.DEPOSIT; data: ValueOf<typeof DepositType>}
83
83
  | {type: SignableMessageType.RANDAO_REVEAL; data: {epoch: Epoch}}
84
84
  | {type: SignableMessageType.VOLUNTARY_EXIT; data: phase0.VoluntaryExit}
package/src/validator.ts CHANGED
@@ -16,14 +16,13 @@ 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";
20
19
  import {ChainHeaderTracker} from "./services/chainHeaderTracker.js";
21
20
  import {DoppelgangerService} from "./services/doppelgangerService.js";
22
21
  import {ValidatorEventEmitter} from "./services/emitter.js";
23
22
  import {ExternalSignerOptions, pollExternalSignerPubkeys} from "./services/externalSignerSync.js";
24
23
  import {IndicesService} from "./services/indices.js";
25
24
  import {pollBuilderValidatorRegistration, pollPrepareBeaconProposer} from "./services/prepareBeaconProposer.js";
26
- import {ProposerPreferencesService} from "./services/proposerPreferences.js";
25
+ import {ProposalPreferencesService} from "./services/proposalPreferences.js";
27
26
  import {PtcService} from "./services/ptc.js";
28
27
  import {SyncCommitteeService} from "./services/syncCommittee.js";
29
28
  import {SyncingStatusTracker} from "./services/syncingStatusTracker.js";
@@ -320,8 +319,16 @@ export class Validator {
320
319
  }
321
320
  );
322
321
 
323
- new ProposerPreferencesService(config, loggerVc, api, clock, validatorStore, blockDutiesService, metrics);
324
- new BuilderPreferencesService(config, loggerVc, api, clock, validatorStore, blockDutiesService, metrics);
322
+ new ProposalPreferencesService(
323
+ config,
324
+ loggerVc,
325
+ api,
326
+ clock,
327
+ validatorStore,
328
+ blockDutiesService,
329
+ syncingStatusTracker,
330
+ metrics
331
+ );
325
332
 
326
333
  return new Validator({
327
334
  opts,
@@ -1,27 +0,0 @@
1
- import { ApiClient } from "@lodestar/api";
2
- import { ChainForkConfig } from "@lodestar/config";
3
- import { IClock } from "@lodestar/state-transition";
4
- import { Metrics } from "../metrics.js";
5
- import { LoggerVc } from "../util/index.js";
6
- import { BlockDutiesService } from "./blockDuties.js";
7
- import { ValidatorStore } from "./validatorStore.js";
8
- /**
9
- * Signs and submits builder preferences for any local validator that will propose within the
10
- * next `SUBMIT_BEFORE_PROPOSAL_SLOTS` and has external builders configured. Signing the request
11
- * auths here also pre-fills the auth cache used at proposal time.
12
- *
13
- * The beacon node submits each entry to the builder at the entry's url. Re-submits automatically
14
- * when the proposer dependent root for an epoch shifts (e.g. after a reorg) since the proposer
15
- * for a slot may have changed.
16
- */
17
- export declare class BuilderPreferencesService {
18
- private readonly config;
19
- private readonly logger;
20
- private readonly api;
21
- private readonly validatorStore;
22
- private readonly blockDutiesService;
23
- private readonly submitted;
24
- constructor(config: ChainForkConfig, logger: LoggerVc, api: ApiClient, clock: IClock, validatorStore: ValidatorStore, blockDutiesService: BlockDutiesService, _metrics: Metrics | null);
25
- private runBuilderPreferencesTask;
26
- }
27
- //# sourceMappingURL=builderPreferences.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"builderPreferences.d.ts","sourceRoot":"","sources":["../../src/services/builderPreferences.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAS,MAAM,eAAe,CAAC;AAChD,OAAO,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAC,MAAM,EAAqB,MAAM,4BAA4B,CAAC;AAGtE,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AACtC,OAAO,EAAC,QAAQ,EAAC,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAC,kBAAkB,EAAC,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAYnD;;;;;;;;GAQG;AACH,qBAAa,yBAAyB;IAIlC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAEpB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IARrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsC;IAEhE,YACmB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE,SAAS,EAC/B,KAAK,EAAE,MAAM,EACI,cAAc,EAAE,cAAc,EAC9B,kBAAkB,EAAE,kBAAkB,EACvD,QAAQ,EAAE,OAAO,GAAG,IAAI,EAGzB;IAED,OAAO,CAAC,yBAAyB,CA+F/B;CACH"}