@lodestar/validator 1.45.0-dev.51a1c44b27 → 1.45.0-dev.6568180f96

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 (39) hide show
  1. package/lib/metrics.d.ts +3 -0
  2. package/lib/metrics.d.ts.map +1 -1
  3. package/lib/metrics.js +5 -0
  4. package/lib/metrics.js.map +1 -1
  5. package/lib/services/block.d.ts +1 -0
  6. package/lib/services/block.d.ts.map +1 -1
  7. package/lib/services/block.js +86 -40
  8. package/lib/services/block.js.map +1 -1
  9. package/lib/services/emitter.d.ts +0 -4
  10. package/lib/services/emitter.d.ts.map +1 -1
  11. package/lib/services/emitter.js +0 -18
  12. package/lib/services/emitter.js.map +1 -1
  13. package/lib/services/proposerPreferences.d.ts +6 -1
  14. package/lib/services/proposerPreferences.d.ts.map +1 -1
  15. package/lib/services/proposerPreferences.js +20 -3
  16. package/lib/services/proposerPreferences.js.map +1 -1
  17. package/lib/services/ptc.d.ts +5 -1
  18. package/lib/services/ptc.d.ts.map +1 -1
  19. package/lib/services/ptc.js +32 -13
  20. package/lib/services/ptc.js.map +1 -1
  21. package/lib/services/validatorStore.d.ts +1 -1
  22. package/lib/services/validatorStore.d.ts.map +1 -1
  23. package/lib/services/validatorStore.js +12 -6
  24. package/lib/services/validatorStore.js.map +1 -1
  25. package/lib/util/params.js +5 -2
  26. package/lib/util/params.js.map +1 -1
  27. package/lib/validator.d.ts +1 -0
  28. package/lib/validator.d.ts.map +1 -1
  29. package/lib/validator.js +3 -0
  30. package/lib/validator.js.map +1 -1
  31. package/package.json +15 -15
  32. package/src/metrics.ts +6 -0
  33. package/src/services/block.ts +111 -51
  34. package/src/services/emitter.ts +0 -21
  35. package/src/services/proposerPreferences.ts +20 -3
  36. package/src/services/ptc.ts +35 -15
  37. package/src/services/validatorStore.ts +22 -7
  38. package/src/util/params.ts +5 -2
  39. package/src/validator.ts +4 -0
@@ -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, isForkPostGloas} from "@lodestar/params";
3
+ import {BUILDER_INDEX_SELF_BUILD, ForkPostGloas, isForkPostGloas} from "@lodestar/params";
4
4
  import {
5
5
  BLSPubkey,
6
6
  BLSSignature,
@@ -39,6 +39,7 @@ type DebugLogCtx = {debugLogCtx: Record<string, string | boolean | undefined>};
39
39
  type BlockProposalOpts = {
40
40
  broadcastValidation: routes.beacon.BroadcastValidation;
41
41
  blindedLocal: boolean;
42
+ payloadLocal: boolean;
42
43
  };
43
44
  /**
44
45
  * Service that sets up and handles validator block proposal duties.
@@ -166,11 +167,19 @@ export class BlockProposingService {
166
167
  }
167
168
 
168
169
  /**
169
- * Gloas stateful block production flow:
170
- * 1. Produce beacon block with execution payload bid
171
- * 2. Sign and publish the beacon block
172
- * 3. Get the execution payload envelope
173
- * 4. Sign and publish the envelope
170
+ * Gloas block production flow:
171
+ * 1. Produce the beacon block, which commits to an execution payload bid. When self-building with
172
+ * the stateless flow (`payloadLocal=false`), the response also includes the full block contents
173
+ * (execution payload envelope, KZG proofs and blobs).
174
+ * 2. Sign and publish the beacon block.
175
+ * 3. Reveal the execution payload envelope:
176
+ * - Self-build: the proposer signs and publishes the envelope
177
+ * - Stateless (`payloadLocal=false`): envelope and blobs are already available from step 1,
178
+ * publish `SignedExecutionPayloadEnvelopeContents` which can be sent via any beacon node
179
+ * - Stateful (`payloadLocal=true`): fetch the envelope from the beacon node that produced the
180
+ * block, then publish the bare `SignedExecutionPayloadEnvelope` back to it; that node
181
+ * attaches the cached blobs and KZG proofs
182
+ * - Builder bid: the builder reveals the envelope, so the proposer does nothing further
174
183
  */
175
184
  private async createAndPublishBlockGloas(pubkey: BLSPubkey, slot: Slot): Promise<void> {
176
185
  const pubkeyHex = toPubkeyHex(pubkey);
@@ -180,8 +189,11 @@ export class BlockProposingService {
180
189
  const randaoReveal = await this.validatorStore.signRandao(pubkey, slot);
181
190
  const graffiti = this.validatorStore.getGraffiti(pubkeyHex);
182
191
  const feeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex);
192
+ const {broadcastValidation, payloadLocal} = this.opts;
193
+ const {selection: builderSelection, boostFactor: builderBoostFactor} =
194
+ this.validatorStore.getBuilderSelectionParams(pubkeyHex, slot);
183
195
 
184
- this.logger.debug("Producing block", {...debugLogCtx, feeRecipient});
196
+ this.logger.debug("Producing block", {...debugLogCtx, feeRecipient, payloadLocal, builderSelection});
185
197
  this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot));
186
198
 
187
199
  // Step 1: Produce beacon block with execution payload bid
@@ -191,19 +203,29 @@ export class BlockProposingService {
191
203
  randaoReveal,
192
204
  graffiti,
193
205
  feeRecipient,
206
+ includePayload: !payloadLocal,
207
+ builderSelection,
208
+ builderBoostFactor,
194
209
  })
195
210
  .catch((e: Error) => {
196
211
  this.metrics?.blockProposingErrors.inc({error: "produce"});
197
212
  throw extendError(e, "Failed to produce block");
198
213
  });
199
- const block = blockRes.value();
214
+ const blockOrContents = blockRes.value();
200
215
  const blockMeta = blockRes.meta();
216
+ const {executionPayloadIncluded} = blockMeta;
217
+ const block = executionPayloadIncluded
218
+ ? (blockOrContents as BlockContents<ForkPostGloas>).block
219
+ : (blockOrContents as BeaconBlock<ForkPostGloas>);
201
220
  const beaconBlockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block);
202
221
  const blockRootHex = toRootHex(beaconBlockRoot);
203
222
 
204
223
  this.logger.debug("Produced block", {
205
224
  ...debugLogCtx,
225
+ executionPayloadValue: prettyWeiToEth(blockMeta.executionPayloadValue),
206
226
  consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue),
227
+ totalBlockValue: prettyWeiToEth(blockMeta.executionPayloadValue + blockMeta.consensusBlockValue),
228
+ executionPayloadIncluded,
207
229
  blockRoot: blockRootHex,
208
230
  });
209
231
  this.metrics?.blocksProduced.inc();
@@ -211,8 +233,6 @@ export class BlockProposingService {
211
233
  // Step 2: Sign and publish the beacon block
212
234
  const signedBlock = await this.validatorStore.signBlock(pubkey, block, slot, this.logger);
213
235
 
214
- const {broadcastValidation} = this.opts;
215
-
216
236
  // Publish the block first so it propagates as soon as possible. This reduces the chance other nodes
217
237
  // see the payload envelope before the block over gossip and have to queue it. There's also plenty of
218
238
  // time left in the slot to propagate the payload, so publishing it in parallel is unnecessary.
@@ -224,63 +244,105 @@ export class BlockProposingService {
224
244
  })
225
245
  .catch((e: Error) => {
226
246
  this.metrics?.blockProposingErrors.inc({error: "publish"});
227
- throw extendError(e, "Failed to publish block");
247
+ throw extendError(e, `Failed to publish block slot=${slot} blockRoot=${blockRootHex}`);
228
248
  })
229
249
  ).assertOk();
230
250
 
231
- this.logger.debug("Published beacon block", {...debugLogCtx, broadcastValidation});
251
+ this.logger.info("Published beacon block", {
252
+ ...logCtx,
253
+ graffiti,
254
+ executionPayloadValue: prettyWeiToEth(blockMeta.executionPayloadValue),
255
+ consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue),
256
+ totalBlockValue: prettyWeiToEth(blockMeta.executionPayloadValue + blockMeta.consensusBlockValue),
257
+ blockRoot: blockRootHex,
258
+ broadcastValidation,
259
+ });
260
+ this.metrics?.proposerStepCallPublishBlock.observe(this.clock.secFromSlot(slot));
261
+ this.metrics?.blocksPublished.inc();
232
262
 
233
263
  const isSelfBuild = block.body.signedExecutionPayloadBid.message.builderIndex === BUILDER_INDEX_SELF_BUILD;
234
264
 
235
265
  if (isSelfBuild) {
236
266
  // Self-build: proposer is responsible for building and publishing the execution payload envelope
237
- // Step 3: Get the execution payload envelope
238
- const envelopeRes = await this.api.validator.getExecutionPayloadEnvelope({
239
- slot,
240
- beaconBlockRoot,
241
- });
242
- const envelope = envelopeRes.value();
243
-
244
- this.logger.debug("Retrieved execution payload envelope", debugLogCtx);
245
-
246
- // Step 4: Sign and publish the envelope
247
- const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(
248
- pubkey,
249
- envelope,
250
- slot,
251
- this.logger
252
- );
253
-
254
- (
255
- await this.api.beacon
256
- .publishExecutionPayloadEnvelope({
257
- signedExecutionPayloadEnvelope: signedEnvelope,
267
+ const flow = executionPayloadIncluded ? "stateless" : "stateful";
268
+ if (executionPayloadIncluded) {
269
+ // Stateless flow: envelope and blobs are already available from block production
270
+ const {executionPayloadEnvelope, kzgProofs, blobs} = blockOrContents as BlockContents<ForkPostGloas>;
271
+
272
+ // Step 3: Sign and publish the envelope with blobs and KZG proofs
273
+ const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(
274
+ pubkey,
275
+ executionPayloadEnvelope,
276
+ slot,
277
+ this.logger
278
+ );
279
+
280
+ (
281
+ await this.api.beacon
282
+ .publishExecutionPayloadEnvelope({
283
+ signedEnvelopeOrContents: {signedExecutionPayloadEnvelope: signedEnvelope, kzgProofs, blobs},
284
+ broadcastValidation,
285
+ })
286
+ .catch((e: Error) => {
287
+ this.metrics?.payloadEnvelopeProposingErrors.inc({error: "publish"});
288
+ throw extendError(
289
+ e,
290
+ `Failed to publish execution payload envelope slot=${slot} blockRoot=${blockRootHex} flow=${flow}`
291
+ );
292
+ })
293
+ ).assertOk();
294
+ } else {
295
+ // Stateful flow: fetch the envelope from the same beacon node that produced the block
296
+ const envelopeRes = await this.api.validator
297
+ .getExecutionPayloadEnvelope({
298
+ slot,
299
+ beaconBlockRoot,
258
300
  })
259
301
  .catch((e: Error) => {
260
- this.metrics?.blockProposingErrors.inc({error: "publish"});
261
- throw extendError(e, "Failed to publish execution payload envelope");
262
- })
263
- ).assertOk();
302
+ this.metrics?.payloadEnvelopeProposingErrors.inc({error: "produce"});
303
+ throw extendError(e, `Failed to get execution payload envelope slot=${slot} blockRoot=${blockRootHex}`);
304
+ });
305
+ const envelope = envelopeRes.value();
306
+
307
+ this.logger.debug("Retrieved execution payload envelope", debugLogCtx);
308
+
309
+ // Step 3: Sign and publish the envelope, beacon node attaches blobs and KZG proofs from its cache
310
+ const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(
311
+ pubkey,
312
+ envelope,
313
+ slot,
314
+ this.logger
315
+ );
316
+
317
+ (
318
+ await this.api.beacon
319
+ .publishExecutionPayloadEnvelope({
320
+ signedEnvelopeOrContents: signedEnvelope,
321
+ broadcastValidation,
322
+ })
323
+ .catch((e: Error) => {
324
+ this.metrics?.payloadEnvelopeProposingErrors.inc({error: "publish"});
325
+ throw extendError(
326
+ e,
327
+ `Failed to publish execution payload envelope slot=${slot} blockRoot=${blockRootHex} flow=${flow}`
328
+ );
329
+ })
330
+ ).assertOk();
331
+ }
264
332
 
265
- this.logger.info("Published block and execution payload envelope", {
333
+ this.logger.info("Published execution payload envelope", {
266
334
  ...logCtx,
267
- graffiti,
268
- consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue),
269
335
  blockRoot: blockRootHex,
336
+ flow,
270
337
  });
271
338
  } else {
272
- // Builder is responsible for broadcasting the execution payload envelope
273
- this.logger.info("Published block with builder bid, envelope expected from builder", {
339
+ // Committed to a builder bid, the builder is responsible for revealing the execution payload envelope
340
+ this.logger.info("Execution payload envelope to be revealed by builder", {
274
341
  ...logCtx,
275
- graffiti,
276
342
  builderIndex: block.body.signedExecutionPayloadBid.message.builderIndex,
277
- consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue),
278
343
  blockRoot: blockRootHex,
279
344
  });
280
345
  }
281
-
282
- this.metrics?.proposerStepCallPublishBlock.observe(this.clock.secFromSlot(slot));
283
- this.metrics?.blocksPublished.inc();
284
346
  }
285
347
 
286
348
  private publishBlockWrapper = async (
@@ -353,10 +415,8 @@ function parseProduceBlockResponse(
353
415
  const executionPayloadSource = response.executionPayloadSource;
354
416
 
355
417
  if (
356
- (builderSelection === routes.validator.BuilderSelection.BuilderOnly &&
357
- executionPayloadSource === ProducedBlockSource.engine) ||
358
- (builderSelection === routes.validator.BuilderSelection.ExecutionOnly &&
359
- executionPayloadSource === ProducedBlockSource.builder)
418
+ builderSelection === routes.validator.BuilderSelection.ExecutionOnly &&
419
+ executionPayloadSource === ProducedBlockSource.builder
360
420
  ) {
361
421
  throw Error(
362
422
  `Block not produced as per desired builderSelection=${builderSelection} executionPayloadSource=${executionPayloadSource}`
@@ -50,25 +50,4 @@ export class ValidatorEventEmitter extends (EventEmitter as {
50
50
  this.on(ValidatorEvent.chainHead, headListener);
51
51
  });
52
52
  }
53
-
54
- /**
55
- * Wait for the first execution payload availability event to come with slot >= provided slot.
56
- */
57
- async waitForExecutionPayloadAvailableSlot(slot: Slot): Promise<void> {
58
- let payloadListener: (payload: ExecutionPayloadAvailableEventData) => void;
59
-
60
- const onDone = (): void => {
61
- this.off(ValidatorEvent.executionPayloadAvailable, payloadListener);
62
- };
63
-
64
- return new Promise((resolve) => {
65
- payloadListener = (payload): void => {
66
- if (payload.slot >= slot) {
67
- onDone();
68
- resolve();
69
- }
70
- };
71
- this.on(ValidatorEvent.executionPayloadAvailable, payloadListener);
72
- });
73
- }
74
53
  }
@@ -29,7 +29,10 @@ type SubmittedAtEpoch = {dependentRoot: RootHex; slots: Set<Slot>};
29
29
  * dependent root for an epoch shifts (e.g. after a reorg) — detected by comparing the cached
30
30
  * `dependentRoot` reported by `BlockDutiesService` against the one we last submitted under.
31
31
  *
32
- * No-op pre-gloas.
32
+ * Proposers should broadcast their preferences before the fork so the proposer preference caches
33
+ * of beacon nodes and builders are warm for the first Gloas slots. We start submitting
34
+ * as soon as a duty's proposal slot is in Gloas, which is up to `SUBMIT_BEFORE_PROPOSAL_SLOTS`
35
+ * before the fork, so only the first few Gloas slots are affected by this pre-fork submission.
33
36
  */
34
37
  export class ProposerPreferencesService {
35
38
  private readonly submitted = new Map<Epoch, SubmittedAtEpoch>();
@@ -44,10 +47,14 @@ export class ProposerPreferencesService {
44
47
  _metrics: Metrics | null
45
48
  ) {
46
49
  clock.runEverySlot(this.runProposerPreferencesTask);
50
+ clock.runEveryEpoch(this.pruneSubmitted);
47
51
  }
48
52
 
49
53
  private runProposerPreferencesTask = async (slot: Slot): Promise<void> => {
50
- if (!isForkPostGloas(this.config.getForkName(slot))) {
54
+ // Start running once the submission window (`slot + SUBMIT_BEFORE_PROPOSAL_SLOTS`) reaches
55
+ // Gloas, i.e. already in the epoch before the fork. This allows builders to prepare and
56
+ // submit bids for the first Gloas slots.
57
+ if (!isForkPostGloas(this.config.getForkName(slot + SUBMIT_BEFORE_PROPOSAL_SLOTS))) {
51
58
  return;
52
59
  }
53
60
 
@@ -82,6 +89,7 @@ export class ProposerPreferencesService {
82
89
  for (const duty of dutiesAtEpoch.data) {
83
90
  if (duty.slot <= slot) continue;
84
91
  if (duty.slot > slot + SUBMIT_BEFORE_PROPOSAL_SLOTS) continue;
92
+ if (!isForkPostGloas(this.config.getForkName(duty.slot))) continue;
85
93
  if (submission.slots.has(duty.slot)) continue;
86
94
 
87
95
  try {
@@ -110,7 +118,7 @@ export class ProposerPreferencesService {
110
118
  }
111
119
 
112
120
  try {
113
- await this.api.beacon.submitSignedProposerPreferences({signedProposerPreferences: batch});
121
+ await this.api.validator.submitProposerPreferences({signedProposerPreferences: batch});
114
122
  // Only mark as submitted after the API call succeeds; a thrown error leaves the
115
123
  // slot eligible for retry on the next tick.
116
124
  for (const {submission, slot: submittedSlot} of pending) {
@@ -121,4 +129,13 @@ export class ProposerPreferencesService {
121
129
  this.logger.error("Error submitting signed proposer preferences", {count: batch.length}, e as Error);
122
130
  }
123
131
  };
132
+
133
+ /** Drop tracking for past epochs; only currentEpoch and currentEpoch + 1 are ever processed. */
134
+ private pruneSubmitted = async (epoch: Epoch): Promise<void> => {
135
+ for (const trackedEpoch of this.submitted.keys()) {
136
+ if (trackedEpoch < epoch) {
137
+ this.submitted.delete(trackedEpoch);
138
+ }
139
+ }
140
+ };
124
141
  }
@@ -1,4 +1,4 @@
1
- import {ApiClient, HttpStatusCode, routes} from "@lodestar/api";
1
+ import {ApiClient, routes} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
3
  import {isForkPostGloas} from "@lodestar/params";
4
4
  import {Slot, gloas} from "@lodestar/types";
@@ -7,7 +7,7 @@ import {Metrics} from "../metrics.js";
7
7
  import {PubkeyHex} from "../types.js";
8
8
  import {IClock, LoggerVc} from "../util/index.js";
9
9
  import {ChainHeaderTracker} from "./chainHeaderTracker.js";
10
- import {ValidatorEventEmitter} from "./emitter.js";
10
+ import {ExecutionPayloadAvailableEventData, ValidatorEvent, ValidatorEventEmitter} from "./emitter.js";
11
11
  import {PtcDutiesService} from "./ptcDuties.js";
12
12
  import {SyncingStatusTracker} from "./syncingStatusTracker.js";
13
13
  import {ValidatorStore} from "./validatorStore.js";
@@ -25,7 +25,7 @@ export class PtcService {
25
25
  private readonly clock: IClock,
26
26
  private readonly validatorStore: ValidatorStore,
27
27
  private readonly emitter: ValidatorEventEmitter,
28
- chainHeadTracker: ChainHeaderTracker,
28
+ private readonly chainHeadTracker: ChainHeaderTracker,
29
29
  syncingStatusTracker: SyncingStatusTracker,
30
30
  private readonly metrics: Metrics | null
31
31
  ) {
@@ -59,19 +59,26 @@ export class PtcService {
59
59
  }
60
60
 
61
61
  const payloadAttestationDueMs = this.config.getSlotComponentDurationMs(this.config.PAYLOAD_ATTESTATION_DUE_BPS);
62
- await Promise.race([
63
- sleep(payloadAttestationDueMs - this.clock.msFromSlot(slot), signal),
64
- this.emitter.waitForExecutionPayloadAvailableSlot(slot),
65
- ]);
62
+ // Submit as soon as the canonical head block's payload is available, or at the deadline
63
+ const payloadAvailable = new AbortController();
64
+ try {
65
+ await Promise.race([
66
+ sleep(payloadAttestationDueMs - this.clock.msFromSlot(slot), signal),
67
+ this.waitForCanonicalPayload(slot, payloadAvailable.signal),
68
+ ]);
69
+ } finally {
70
+ payloadAvailable.abort();
71
+ }
66
72
 
67
73
  this.metrics?.ptcStepCallProducePayloadAttestation.observe(
68
74
  this.clock.secFromSlot(slot) - payloadAttestationDueMs / 1000
69
75
  );
70
76
 
71
77
  try {
72
- const payloadAttestationData = await this.producePayloadAttestationData(slot);
78
+ // No canonical block at slot resolves to `undefined`
79
+ const payloadAttestationData = (await this.api.validator.producePayloadAttestationData({slot})).value();
73
80
  // If no beacon block was seen for the assigned slot, do not submit a payload attestation
74
- if (payloadAttestationData === null) {
81
+ if (!payloadAttestationData) {
75
82
  this.logger.debug("Skipping payload attestation, no beacon block seen for slot", {slot});
76
83
  return;
77
84
  }
@@ -81,12 +88,25 @@ export class PtcService {
81
88
  }
82
89
  };
83
90
 
84
- private async producePayloadAttestationData(slot: Slot): Promise<gloas.PayloadAttestationData | null> {
85
- const res = await this.api.validator.producePayloadAttestationData({slot});
86
- if (!res.ok && res.status === HttpStatusCode.NOT_FOUND) {
87
- return null;
88
- }
89
- return res.value();
91
+ /**
92
+ * Resolve when the payload for the canonical head block at `slot` is available
93
+ */
94
+ private waitForCanonicalPayload(slot: Slot, signal: AbortSignal): Promise<void> {
95
+ return new Promise((resolve) => {
96
+ const onPayloadAvailable = (payload: ExecutionPayloadAvailableEventData): void => {
97
+ const head = this.chainHeadTracker.getCurrentChainHead(slot);
98
+ if (payload.slot === slot && head !== null && payload.blockRoot === toRootHex(head)) {
99
+ this.emitter.off(ValidatorEvent.executionPayloadAvailable, onPayloadAvailable);
100
+ resolve();
101
+ }
102
+ };
103
+ signal.addEventListener(
104
+ "abort",
105
+ () => this.emitter.off(ValidatorEvent.executionPayloadAvailable, onPayloadAvailable),
106
+ {once: true}
107
+ );
108
+ this.emitter.on(ValidatorEvent.executionPayloadAvailable, onPayloadAvailable);
109
+ });
90
110
  }
91
111
 
92
112
  private async signAndPublishPayloadAttestations(
@@ -81,7 +81,8 @@ type DefaultProposerConfig = {
81
81
  feeRecipient: ExecutionAddress;
82
82
  builder: {
83
83
  gasLimit: number;
84
- selection: routes.validator.BuilderSelection;
84
+ // Left undefined when not configured so the fork-appropriate default can be resolved per slot
85
+ selection?: routes.validator.BuilderSelection;
85
86
  boostFactor: bigint;
86
87
  };
87
88
  };
@@ -182,7 +183,7 @@ export class ValidatorStore {
182
183
  feeRecipient: defaultConfig.feeRecipient ?? defaultOptions.suggestedFeeRecipient,
183
184
  builder: {
184
185
  gasLimit: defaultConfig.builder?.gasLimit ?? defaultOptions.defaultGasLimit,
185
- selection: defaultConfig.builder?.selection ?? defaultOptions.builderSelection,
186
+ selection: defaultConfig.builder?.selection,
186
187
  boostFactor: builderBoostFactor,
187
188
  },
188
189
  };
@@ -278,9 +279,21 @@ export class ValidatorStore {
278
279
  delete validatorData.graffiti;
279
280
  }
280
281
 
281
- getBuilderSelectionParams(pubkeyHex: PubkeyHex): {selection: routes.validator.BuilderSelection; boostFactor: bigint} {
282
+ getBuilderSelectionParams(
283
+ pubkeyHex: PubkeyHex,
284
+ slot?: Slot
285
+ ): {selection: routes.validator.BuilderSelection; boostFactor: bigint} {
286
+ // Builder bids post-gloas are in-protocol over p2p, so the default strategy uses them
287
+ // (as if `--builder` was set), unless the validator explicitly opted out. Pre-gloas
288
+ // there is no in-protocol builder, so the default remains local-only (executiononly).
289
+ const defaultSelection =
290
+ slot !== undefined && this.config.getForkSeq(slot) >= ForkSeq.gloas
291
+ ? defaultOptions.builderAliasSelection
292
+ : defaultOptions.builderSelection;
282
293
  const selection =
283
- this.validators.get(pubkeyHex)?.builder?.selection ?? this.defaultProposerConfig.builder.selection;
294
+ this.validators.get(pubkeyHex)?.builder?.selection ??
295
+ this.defaultProposerConfig.builder.selection ??
296
+ defaultSelection;
284
297
 
285
298
  let boostFactor: bigint;
286
299
  switch (selection) {
@@ -607,9 +620,11 @@ export class ValidatorStore {
607
620
  const signingSlot = aggregate.data.slot;
608
621
  const domain = this.config.getDomain(signingSlot, DOMAIN_AGGREGATE_AND_PROOF);
609
622
  const isPostElectra = this.config.getForkSeq(signingSlot) >= ForkSeq.electra;
610
- const signingRoot = isPostElectra
611
- ? computeSigningRoot(ssz.electra.AggregateAndProof, aggregateAndProof, domain)
612
- : computeSigningRoot(ssz.phase0.AggregateAndProof, aggregateAndProof, domain);
623
+ const signingRoot = computeSigningRoot(
624
+ this.config.getForkTypes(signingSlot).AggregateAndProof,
625
+ aggregateAndProof,
626
+ domain
627
+ );
613
628
 
614
629
  const signableMessage: SignableMessage = {
615
630
  type: isPostElectra ? SignableMessageType.AGGREGATE_AND_PROOF_V2 : SignableMessageType.AGGREGATE_AND_PROOF,
@@ -326,9 +326,12 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record<keyof ConfigWit
326
326
  MAX_PAYLOAD_ATTESTATIONS: gloasForkRelevant,
327
327
  MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: gloasForkRelevant,
328
328
  MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: gloasForkRelevant,
329
- BUILDER_REGISTRY_LIMIT: gloasForkRelevant,
330
- BUILDER_PENDING_WITHDRAWALS_LIMIT: gloasForkRelevant,
331
329
  MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: gloasForkRelevant,
330
+ MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE: false,
331
+ MAX_ATTESTER_SLASHING_SIZE: false,
332
+ MAX_DATA_COLUMN_SIDECAR_SIZE: false,
333
+ MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE: false,
334
+ MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE: false,
332
335
  MIN_BUILDER_WITHDRAWABILITY_DELAY: gloasForkRelevant,
333
336
 
334
337
  // FastConfirmationRule
package/src/validator.ts CHANGED
@@ -66,6 +66,7 @@ export type ValidatorOptions = {
66
66
  distributed?: boolean;
67
67
  broadcastValidation?: routes.beacon.BroadcastValidation;
68
68
  blindedLocal?: boolean;
69
+ payloadLocal?: boolean;
69
70
  externalSigner?: ExternalSignerOptions;
70
71
  clock?: ClockOptions;
71
72
  };
@@ -256,6 +257,9 @@ export class Validator {
256
257
  {
257
258
  broadcastValidation: opts.broadcastValidation ?? defaultOptions.broadcastValidation,
258
259
  blindedLocal: opts.blindedLocal ?? defaultOptions.blindedLocal,
260
+ // Default to keeping the payload local to the beacon node if only a single node is
261
+ // configured, with multiple nodes the stateless flow allows publishing via any of them
262
+ payloadLocal: opts.payloadLocal ?? api.httpClient.urlsInits.length <= 1,
259
263
  }
260
264
  );
261
265