@lodestar/validator 1.39.0-dev.3bf4734ba9 → 1.39.0-dev.4263377242

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.
@@ -1,8 +1,8 @@
1
- import {ApiClient, routes} from "@lodestar/api";
1
+ import {ApiClient} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
3
  import {ForkName, isForkPostElectra} from "@lodestar/params";
4
- import {computeEpochAtSlot, isAggregatorFromCommitteeLength} from "@lodestar/state-transition";
5
- import {BLSSignature, SignedAggregateAndProof, SingleAttestation, Slot, phase0, ssz} from "@lodestar/types";
4
+ import {computeEpochAtSlot} from "@lodestar/state-transition";
5
+ import {SignedAggregateAndProof, SingleAttestation, Slot, phase0, ssz} from "@lodestar/types";
6
6
  import {prettyBytes, sleep, toRootHex} from "@lodestar/utils";
7
7
  import {Metrics} from "../metrics.js";
8
8
  import {PubkeyHex} from "../types.js";
@@ -75,18 +75,6 @@ export class AttestationService {
75
75
  }
76
76
  const fork = this.config.getForkName(slot);
77
77
 
78
- if (this.opts?.distributedAggregationSelection) {
79
- // Validator in distributed cluster only has a key share, not the full private key.
80
- // The partial selection proofs must be exchanged for combined selection proofs by
81
- // calling submitBeaconCommitteeSelections on the distributed validator middleware client.
82
- // This will run in parallel to other attestation tasks but must be finished before starting
83
- // attestation aggregation as it is required to correctly determine if validator is aggregator
84
- // and to produce a AggregateAndProof that can be threshold aggregated by the middleware client.
85
- this.runDistributedAggregationSelectionTasks(fork, duties, slot, signal).catch((e) =>
86
- this.logger.error("Error on attestation aggregation selection", {slot}, e)
87
- );
88
- }
89
-
90
78
  // A validator should create and broadcast the attestation to the associated attestation subnet when either
91
79
  // (a) the validator has received a valid block from the expected block proposer for the assigned slot or
92
80
  // (b) ATTESTATION_DUE_BPS of the slot has transpired -- whichever comes first.
@@ -274,89 +262,4 @@ export class AttestationService {
274
262
  }
275
263
  }
276
264
  }
277
-
278
- /**
279
- * Performs additional attestation aggregation tasks required if validator is part of distributed cluster
280
- *
281
- * 1. Exchange partial for combined selection proofs
282
- * 2. Determine validators that should aggregate attestations
283
- * 3. Mutate duty objects to set selection proofs for aggregators
284
- * 4. Resubscribe validators as aggregators on beacon committee subnets
285
- *
286
- * See https://docs.google.com/document/d/1q9jOTPcYQa-3L8luRvQJ-M0eegtba4Nmon3dpO79TMk/mobilebasic
287
- */
288
- private async runDistributedAggregationSelectionTasks(
289
- fork: ForkName,
290
- duties: AttDutyAndProof[],
291
- slot: number,
292
- signal: AbortSignal
293
- ): Promise<void> {
294
- const partialSelections: routes.validator.BeaconCommitteeSelection[] = duties.map(
295
- ({duty, partialSelectionProof}) => ({
296
- validatorIndex: duty.validatorIndex,
297
- slot,
298
- selectionProof: partialSelectionProof as BLSSignature,
299
- })
300
- );
301
-
302
- this.logger.debug("Submitting partial beacon committee selection proofs", {slot, count: partialSelections.length});
303
-
304
- const res = await Promise.race([
305
- this.api.validator.submitBeaconCommitteeSelections({selections: partialSelections}),
306
- // Exit attestation aggregation flow if there is no response after ATTESTATION_DUE_BPS of the slot as
307
- // beacon node would likely not have enough time to prepare an aggregate attestation.
308
- // Note that the aggregations flow is not explicitly exited but rather will be skipped
309
- // due to the fact that calculation of `is_aggregator` in AttestationDutiesService is not done
310
- // and selectionProof is set to null, meaning no validator will be considered an aggregator.
311
- sleep(this.config.getAttestationDueMs(fork) - this.clock.msFromSlot(slot), signal),
312
- ]);
313
-
314
- if (!res) {
315
- throw new Error("Failed to receive combined selection proofs before ATTESTATION_DUE_BPS of the slot");
316
- }
317
-
318
- const combinedSelections = res.value();
319
- this.logger.debug("Received combined beacon committee selection proofs", {slot, count: combinedSelections.length});
320
-
321
- const beaconCommitteeSubscriptions: routes.validator.BeaconCommitteeSubscription[] = [];
322
-
323
- for (const dutyAndProof of duties) {
324
- const {validatorIndex, committeeIndex, committeeLength, committeesAtSlot} = dutyAndProof.duty;
325
- const logCtxValidator = {slot, index: committeeIndex, validatorIndex};
326
-
327
- const combinedSelection = combinedSelections.find((s) => s.validatorIndex === validatorIndex && s.slot === slot);
328
-
329
- if (!combinedSelection) {
330
- this.logger.warn("Did not receive combined beacon committee selection proof", logCtxValidator);
331
- continue;
332
- }
333
-
334
- const isAggregator = isAggregatorFromCommitteeLength(committeeLength, combinedSelection.selectionProof);
335
-
336
- if (isAggregator) {
337
- // Update selection proof by mutating duty object
338
- dutyAndProof.selectionProof = combinedSelection.selectionProof;
339
-
340
- // Only push subnet subscriptions with `isAggregator=true` as all validators
341
- // with duties for slot are already subscribed to subnets with `isAggregator=false`.
342
- beaconCommitteeSubscriptions.push({
343
- validatorIndex,
344
- committeesAtSlot,
345
- committeeIndex,
346
- slot,
347
- isAggregator,
348
- });
349
- this.logger.debug("Resubscribing validator as aggregator on beacon committee subnet", logCtxValidator);
350
- }
351
- }
352
-
353
- // If there are any subscriptions with aggregators, push them out to the beacon node.
354
- if (beaconCommitteeSubscriptions.length > 0) {
355
- (await this.api.validator.prepareBeaconCommitteeSubnet({subscriptions: beaconCommitteeSubscriptions})).assertOk();
356
- this.logger.debug("Resubscribed validators as aggregators on beacon committee subnets", {
357
- slot,
358
- count: beaconCommitteeSubscriptions.length,
359
- });
360
- }
361
- }
362
265
  }
@@ -204,6 +204,17 @@ export class AttestationDutiesService {
204
204
  for (const epoch of [currentEpoch, nextEpoch]) {
205
205
  const epochDuties = this.dutiesByIndexByEpoch.get(epoch)?.dutiesByIndex;
206
206
  if (epochDuties) {
207
+ if (this.opts?.distributedAggregationSelection) {
208
+ // Validator in distributed cluster only has a key share, not the full private key.
209
+ // The partial selection proofs must be exchanged for combined selection proofs by
210
+ // calling submitBeaconCommitteeSelections on the distributed validator middleware client.
211
+ // This is required to correctly determine if validator is aggregator and to produce
212
+ // a AggregateAndProof that can be threshold aggregated by the middleware client.
213
+ await this.runDistributedAggregationSelectionTasks(Array.from(epochDuties.values()), epoch).catch((e) =>
214
+ this.logger.debug("Error on attestation aggregation selection", {epoch}, e)
215
+ );
216
+ }
217
+
207
218
  for (const {duty, selectionProof} of epochDuties.values()) {
208
219
  if (indexSet.has(duty.validatorIndex)) {
209
220
  beaconCommitteeSubscriptions.push({
@@ -367,6 +378,12 @@ export class AttestationDutiesService {
367
378
  const epochDuties = this.dutiesByIndexByEpoch.get(dutyEpoch)?.dutiesByIndex;
368
379
 
369
380
  if (epochDuties) {
381
+ if (this.opts?.distributedAggregationSelection) {
382
+ await this.runDistributedAggregationSelectionTasks(Array.from(epochDuties.values()), dutyEpoch).catch((e) =>
383
+ this.logger.debug("Error on attestation aggregation selection after duties reorg", logContext, e)
384
+ );
385
+ }
386
+
370
387
  for (const {duty, selectionProof} of epochDuties.values()) {
371
388
  beaconCommitteeSubscriptions.push({
372
389
  validatorIndex: duty.validatorIndex,
@@ -403,8 +420,8 @@ export class AttestationDutiesService {
403
420
  if (this.opts?.distributedAggregationSelection) {
404
421
  // Validator in distributed cluster only has a key share, not the full private key.
405
422
  // Passing a partial selection proof to `is_aggregator` would produce incorrect result.
406
- // AttestationService will exchange partial for combined selection proofs retrieved from
407
- // distributed validator middleware client and determine aggregators at beginning of every slot.
423
+ // Before subscribing to beacon committee subnets, aggregators are determined by exchanging
424
+ // partial for combined selection proofs retrieved from distributed validator middleware client.
408
425
  return {duty, selectionProof: null, partialSelectionProof: selectionProof};
409
426
  }
410
427
 
@@ -427,4 +444,64 @@ export class AttestationDutiesService {
427
444
  }
428
445
  }
429
446
  }
447
+
448
+ /**
449
+ * Performs additional attestation aggregation tasks required if validator is part of distributed cluster
450
+ *
451
+ * 1. Exchange partial for combined selection proofs
452
+ * 2. Determine validators that should aggregate attestations
453
+ * 3. Mutate duty objects to set selection proofs for aggregators
454
+ */
455
+ private async runDistributedAggregationSelectionTasks(duties: AttDutyAndProof[], epoch: Epoch): Promise<void> {
456
+ if (duties.length === 0) {
457
+ return;
458
+ }
459
+
460
+ const partialSelections: routes.validator.BeaconCommitteeSelection[] = duties.map(
461
+ ({duty, partialSelectionProof}) => ({
462
+ validatorIndex: duty.validatorIndex,
463
+ slot: duty.slot,
464
+ selectionProof: partialSelectionProof as BLSSignature,
465
+ })
466
+ );
467
+
468
+ this.logger.debug("Submitting partial beacon committee selection proofs", {epoch, count: partialSelections.length});
469
+
470
+ const res = await this.api.validator.submitBeaconCommitteeSelections({selections: partialSelections});
471
+
472
+ const combinedSelections = new Map<ValidatorIndex, routes.validator.BeaconCommitteeSelection>();
473
+ for (const selection of res.value()) {
474
+ combinedSelections.set(selection.validatorIndex, selection);
475
+ }
476
+ this.logger.debug("Received combined beacon committee selection proofs", {epoch, count: combinedSelections.size});
477
+
478
+ for (const dutyAndProof of duties) {
479
+ const {slot, validatorIndex, committeeIndex, committeeLength} = dutyAndProof.duty;
480
+ const logCtxValidator = {slot, index: committeeIndex, validatorIndex};
481
+
482
+ const combinedSelection = combinedSelections.get(validatorIndex);
483
+
484
+ if (!combinedSelection) {
485
+ this.logger.debug("Did not receive combined beacon committee selection proof", logCtxValidator);
486
+ continue;
487
+ }
488
+
489
+ if (combinedSelection.slot !== slot) {
490
+ this.logger.debug("Received combined beacon committee selection proof for different slot", {
491
+ expected: slot,
492
+ actual: combinedSelection.slot,
493
+ index: committeeIndex,
494
+ validatorIndex,
495
+ });
496
+ continue;
497
+ }
498
+
499
+ const isAggregator = isAggregatorFromCommitteeLength(committeeLength, combinedSelection.selectionProof);
500
+
501
+ if (isAggregator) {
502
+ // Update selection proof by mutating duty object
503
+ dutyAndProof.selectionProof = combinedSelection.selectionProof;
504
+ }
505
+ }
506
+ }
430
507
  }
@@ -1,8 +1,7 @@
1
- import {ApiClient, routes} from "@lodestar/api";
1
+ import {ApiClient} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
3
  import {ForkName, isForkPostAltair} from "@lodestar/params";
4
- import {isSyncCommitteeAggregator} from "@lodestar/state-transition";
5
- import {BLSSignature, CommitteeIndex, Root, Slot, altair} from "@lodestar/types";
4
+ import {CommitteeIndex, Root, Slot, altair} from "@lodestar/types";
6
5
  import {sleep} from "@lodestar/utils";
7
6
  import {Metrics} from "../metrics.js";
8
7
  import {PubkeyHex} from "../types.js";
@@ -73,18 +72,6 @@ export class SyncCommitteeService {
73
72
  return;
74
73
  }
75
74
 
76
- if (this.opts?.distributedAggregationSelection) {
77
- // Validator in distributed cluster only has a key share, not the full private key.
78
- // The partial selection proofs must be exchanged for combined selection proofs by
79
- // calling submitSyncCommitteeSelections on the distributed validator middleware client.
80
- // This will run in parallel to other sync committee tasks but must be finished before starting
81
- // sync committee contributions as it is required to correctly determine if validator is aggregator
82
- // and to produce a ContributionAndProof that can be threshold aggregated by the middleware client.
83
- this.runDistributedAggregationSelectionTasks(fork, dutiesAtSlot, slot, signal).catch((e) =>
84
- this.logger.error("Error on sync committee aggregation selection", {slot}, e)
85
- );
86
- }
87
-
88
75
  // unlike Attestation, SyncCommitteeSignature could be published asap
89
76
  // especially with lodestar, it's very busy at ATTESTATION_DUE_BPS of the slot
90
77
  // see https://github.com/ChainSafe/lodestar/issues/4608
@@ -257,82 +244,4 @@ export class SyncCommitteeService {
257
244
  }
258
245
  }
259
246
  }
260
-
261
- /**
262
- * Performs additional sync committee contribution tasks required if validator is part of distributed cluster
263
- *
264
- * 1. Exchange partial for combined selection proofs
265
- * 2. Determine validators that should produce sync committee contribution
266
- * 3. Mutate duty objects to set selection proofs for aggregators
267
- *
268
- * See https://docs.google.com/document/d/1q9jOTPcYQa-3L8luRvQJ-M0eegtba4Nmon3dpO79TMk/mobilebasic
269
- */
270
- private async runDistributedAggregationSelectionTasks(
271
- fork: ForkName,
272
- duties: SyncDutyAndProofs[],
273
- slot: number,
274
- signal: AbortSignal
275
- ): Promise<void> {
276
- const partialSelections: routes.validator.SyncCommitteeSelection[] = [];
277
-
278
- for (const {duty, selectionProofs} of duties) {
279
- const validatorSelections: routes.validator.SyncCommitteeSelection[] = selectionProofs.map(
280
- ({subcommitteeIndex, partialSelectionProof}) => ({
281
- validatorIndex: duty.validatorIndex,
282
- slot,
283
- subcommitteeIndex,
284
- selectionProof: partialSelectionProof as BLSSignature,
285
- })
286
- );
287
- partialSelections.push(...validatorSelections);
288
- }
289
-
290
- this.logger.debug("Submitting partial sync committee selection proofs", {slot, count: partialSelections.length});
291
-
292
- const res = await Promise.race([
293
- this.api.validator.submitSyncCommitteeSelections({selections: partialSelections}),
294
- // Exit sync committee contributions flow if there is no response after CONTRIBUTION_DUE_BPS of the slot.
295
- // This is in contrast to attestations aggregations flow which is already exited at ATTESTATION_DUE_BPS of the slot
296
- // because for sync committee is not required to resubscribe to subnets as beacon node will assume
297
- // validator always aggregates. This allows us to wait until we have to produce sync committee contributions.
298
- // Note that the sync committee contributions flow is not explicitly exited but rather will be skipped
299
- // due to the fact that calculation of `is_sync_committee_aggregator` in SyncCommitteeDutiesService is not done
300
- // and selectionProof is set to null, meaning no validator will be considered an aggregator.
301
- sleep(this.config.getSyncContributionDueMs(fork) - this.clock.msFromSlot(slot), signal),
302
- ]);
303
-
304
- if (!res) {
305
- throw new Error("Failed to receive combined selection proofs before CONTRIBUTION_DUE_BPS of the slot");
306
- }
307
-
308
- const combinedSelections = res.value();
309
- this.logger.debug("Received combined sync committee selection proofs", {slot, count: combinedSelections.length});
310
-
311
- for (const dutyAndProofs of duties) {
312
- const {validatorIndex, subnets} = dutyAndProofs.duty;
313
-
314
- for (const subnet of subnets) {
315
- const logCtxValidator = {slot, index: subnet, validatorIndex};
316
-
317
- const combinedSelection = combinedSelections.find(
318
- (s) => s.validatorIndex === validatorIndex && s.slot === slot && s.subcommitteeIndex === subnet
319
- );
320
-
321
- if (!combinedSelection) {
322
- this.logger.warn("Did not receive combined sync committee selection proof", logCtxValidator);
323
- continue;
324
- }
325
-
326
- const isAggregator = isSyncCommitteeAggregator(combinedSelection.selectionProof);
327
-
328
- if (isAggregator) {
329
- const selectionProofObject = dutyAndProofs.selectionProofs.find((p) => p.subcommitteeIndex === subnet);
330
- if (selectionProofObject) {
331
- // Update selection proof by mutating proof objects in duty object
332
- selectionProofObject.selectionProof = combinedSelection.selectionProof;
333
- }
334
- }
335
- }
336
- }
337
- }
338
247
  }
@@ -85,7 +85,7 @@ export class SyncCommitteeDutiesService {
85
85
  private readonly config: ChainForkConfig,
86
86
  private readonly logger: LoggerVc,
87
87
  private readonly api: ApiClient,
88
- clock: IClock,
88
+ private readonly clock: IClock,
89
89
  private readonly validatorStore: ValidatorStore,
90
90
  syncingStatusTracker: SyncingStatusTracker,
91
91
  metrics: Metrics | null,
@@ -134,6 +134,18 @@ export class SyncCommitteeDutiesService {
134
134
  selectionProofs: await this.getSelectionProofs(slot, dutyAtPeriod.duty),
135
135
  });
136
136
  }
137
+
138
+ if (this.opts?.distributedAggregationSelection) {
139
+ // Validator in distributed cluster only has a key share, not the full private key.
140
+ // The partial selection proofs must be exchanged for combined selection proofs by
141
+ // calling submitSyncCommitteeSelections on the distributed validator middleware client.
142
+ // This will run in parallel to other sync committee tasks but must be finished before starting
143
+ // sync committee contributions as it is required to correctly determine if validator is aggregator
144
+ // and to produce a ContributionAndProof that can be threshold aggregated by the middleware client.
145
+ this.runDistributedAggregationSelectionTasks(duties, slot).catch((e) =>
146
+ this.logger.debug("Error on sync committee aggregation selection", {slot}, e)
147
+ );
148
+ }
137
149
  }
138
150
 
139
151
  return duties;
@@ -307,8 +319,8 @@ export class SyncCommitteeDutiesService {
307
319
  if (this.opts?.distributedAggregationSelection) {
308
320
  // Validator in distributed cluster only has a key share, not the full private key.
309
321
  // Passing a partial selection proof to `is_sync_committee_aggregator` would produce incorrect result.
310
- // SyncCommitteeService will exchange partial for combined selection proofs retrieved from
311
- // distributed validator middleware client and determine aggregators at beginning of every slot.
322
+ // For all duties in the slot, aggregators are determined by exchanging partial for combined selection
323
+ // proofs retrieved from distributed validator middleware client at beginning of every slot.
312
324
  dutiesAndProofs.push({
313
325
  selectionProof: null,
314
326
  partialSelectionProof: selectionProof,
@@ -334,4 +346,74 @@ export class SyncCommitteeDutiesService {
334
346
  }
335
347
  }
336
348
  }
349
+
350
+ /**
351
+ * Performs additional sync committee contribution tasks required if validator is part of distributed cluster
352
+ *
353
+ * 1. Exchange partial for combined selection proofs
354
+ * 2. Determine validators that should produce sync committee contribution
355
+ * 3. Mutate duty objects to set selection proofs for aggregators
356
+ */
357
+ private async runDistributedAggregationSelectionTasks(duties: SyncDutyAndProofs[], slot: number): Promise<void> {
358
+ if (duties.length === 0) {
359
+ return;
360
+ }
361
+
362
+ const partialSelections: routes.validator.SyncCommitteeSelection[] = [];
363
+
364
+ for (const {duty, selectionProofs} of duties) {
365
+ const validatorSelections: routes.validator.SyncCommitteeSelection[] = selectionProofs.map(
366
+ ({subcommitteeIndex, partialSelectionProof}) => ({
367
+ validatorIndex: duty.validatorIndex,
368
+ slot,
369
+ subcommitteeIndex,
370
+ selectionProof: partialSelectionProof as BLSSignature,
371
+ })
372
+ );
373
+ partialSelections.push(...validatorSelections);
374
+ }
375
+
376
+ this.logger.debug("Submitting partial sync committee selection proofs", {slot, count: partialSelections.length});
377
+
378
+ const res = await this.api.validator.submitSyncCommitteeSelections(
379
+ {selections: partialSelections},
380
+ {
381
+ // Exit sync committee contributions flow if there is no response until CONTRIBUTION_DUE_BPS of the slot.
382
+ // Note that the sync committee contributions flow is not explicitly exited but rather will be skipped
383
+ // due to the fact that calculation of `is_sync_committee_aggregator` in SyncCommitteeDutiesService is not done
384
+ // and selectionProof is set to null, meaning no validator will be considered an aggregator.
385
+ timeoutMs: this.config.getSyncContributionDueMs(this.config.getForkName(slot)) - this.clock.msFromSlot(slot),
386
+ }
387
+ );
388
+
389
+ const combinedSelections = res.value();
390
+ this.logger.debug("Received combined sync committee selection proofs", {slot, count: combinedSelections.length});
391
+
392
+ for (const dutyAndProofs of duties) {
393
+ const {validatorIndex, subnets} = dutyAndProofs.duty;
394
+
395
+ for (const subnet of subnets) {
396
+ const logCtxValidator = {slot, index: subnet, validatorIndex};
397
+
398
+ const combinedSelection = combinedSelections.find(
399
+ (s) => s.validatorIndex === validatorIndex && s.slot === slot && s.subcommitteeIndex === subnet
400
+ );
401
+
402
+ if (!combinedSelection) {
403
+ this.logger.debug("Did not receive combined sync committee selection proof", logCtxValidator);
404
+ continue;
405
+ }
406
+
407
+ const isAggregator = isSyncCommitteeAggregator(combinedSelection.selectionProof);
408
+
409
+ if (isAggregator) {
410
+ const selectionProofObject = dutyAndProofs.selectionProofs.find((p) => p.subcommitteeIndex === subnet);
411
+ if (selectionProofObject) {
412
+ // Update selection proof by mutating proof objects in duty object
413
+ selectionProofObject.selectionProof = combinedSelection.selectionProof;
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
337
419
  }
@@ -53,7 +53,7 @@ import {DoppelgangerService} from "./doppelgangerService.js";
53
53
  import {IndicesService} from "./indices.js";
54
54
 
55
55
  type BLSPubkeyMaybeHex = BLSPubkey | PubkeyHex;
56
- type Eth1Address = string;
56
+ type ExecutionAddress = string;
57
57
 
58
58
  export enum SignerType {
59
59
  Local,
@@ -74,7 +74,7 @@ export type SignerRemote = {
74
74
  type DefaultProposerConfig = {
75
75
  graffiti?: string;
76
76
  strictFeeRecipientCheck: boolean;
77
- feeRecipient: Eth1Address;
77
+ feeRecipient: ExecutionAddress;
78
78
  builder: {
79
79
  gasLimit: number;
80
80
  selection: routes.validator.BuilderSelection;
@@ -85,7 +85,7 @@ type DefaultProposerConfig = {
85
85
  export type ProposerConfig = {
86
86
  graffiti?: string;
87
87
  strictFeeRecipientCheck?: boolean;
88
- feeRecipient?: Eth1Address;
88
+ feeRecipient?: ExecutionAddress;
89
89
  builder?: {
90
90
  gasLimit?: number;
91
91
  selection?: routes.validator.BuilderSelection;
@@ -219,7 +219,7 @@ export class ValidatorStore {
219
219
  : this.indicesService.pollValidatorIndices(Array.from(this.validators.keys()));
220
220
  }
221
221
 
222
- getFeeRecipient(pubkeyHex: PubkeyHex): Eth1Address {
222
+ getFeeRecipient(pubkeyHex: PubkeyHex): ExecutionAddress {
223
223
  const validatorData = this.validators.get(pubkeyHex);
224
224
  if (validatorData === undefined) {
225
225
  throw Error(`Validator pubkey ${pubkeyHex} not known`);
@@ -227,12 +227,12 @@ export class ValidatorStore {
227
227
  return validatorData.feeRecipient ?? this.defaultProposerConfig.feeRecipient;
228
228
  }
229
229
 
230
- getFeeRecipientByIndex(index: ValidatorIndex): Eth1Address {
230
+ getFeeRecipientByIndex(index: ValidatorIndex): ExecutionAddress {
231
231
  const pubkey = this.indicesService.index2pubkey.get(index);
232
232
  return pubkey ? this.getFeeRecipient(pubkey) : this.defaultProposerConfig.feeRecipient;
233
233
  }
234
234
 
235
- setFeeRecipient(pubkeyHex: PubkeyHex, feeRecipient: Eth1Address): void {
235
+ setFeeRecipient(pubkeyHex: PubkeyHex, feeRecipient: ExecutionAddress): void {
236
236
  const validatorData = this.validators.get(pubkeyHex);
237
237
  if (validatorData === undefined) {
238
238
  throw Error(`Validator pubkey ${pubkeyHex} not known`);
@@ -696,7 +696,7 @@ export class ValidatorStore {
696
696
 
697
697
  async signValidatorRegistration(
698
698
  pubkeyMaybeHex: BLSPubkeyMaybeHex,
699
- regAttributes: {feeRecipient: Eth1Address; gasLimit: number},
699
+ regAttributes: {feeRecipient: ExecutionAddress; gasLimit: number},
700
700
  _slot: Slot
701
701
  ): Promise<bellatrix.SignedValidatorRegistrationV1> {
702
702
  const pubkey = typeof pubkeyMaybeHex === "string" ? fromHex(pubkeyMaybeHex) : pubkeyMaybeHex;
@@ -727,7 +727,7 @@ export class ValidatorStore {
727
727
 
728
728
  async getValidatorRegistration(
729
729
  pubkeyMaybeHex: BLSPubkeyMaybeHex,
730
- regAttributes: {feeRecipient: Eth1Address; gasLimit: number},
730
+ regAttributes: {feeRecipient: ExecutionAddress; gasLimit: number},
731
731
  slot: Slot
732
732
  ): Promise<bellatrix.SignedValidatorRegistrationV1> {
733
733
  const pubkeyHex = typeof pubkeyMaybeHex === "string" ? pubkeyMaybeHex : toPubkeyHex(pubkeyMaybeHex);
@@ -1,6 +1,6 @@
1
1
  import {ContainerType, ValueOf} from "@chainsafe/ssz";
2
2
  import {BeaconConfig} from "@lodestar/config";
3
- import {ForkPreBellatrix, ForkSeq} from "@lodestar/params";
3
+ import {ForkName, ForkPreBellatrix, ForkSeq, isForkPostDeneb} from "@lodestar/params";
4
4
  import {blindedOrFullBlockToHeader, computeEpochAtSlot} from "@lodestar/state-transition";
5
5
  import {
6
6
  AggregateAndProof,
@@ -11,7 +11,6 @@ import {
11
11
  RootHex,
12
12
  Slot,
13
13
  altair,
14
- capella,
15
14
  phase0,
16
15
  ssz,
17
16
  sszTypesFor,
@@ -33,7 +32,6 @@ export enum SignableMessageType {
33
32
  SYNC_COMMITTEE_SELECTION_PROOF = "SYNC_COMMITTEE_SELECTION_PROOF",
34
33
  SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF = "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF",
35
34
  VALIDATOR_REGISTRATION = "VALIDATOR_REGISTRATION",
36
- BLS_TO_EXECUTION_CHANGE = "BLS_TO_EXECUTION_CHANGE",
37
35
  }
38
36
 
39
37
  const AggregationSlotType = new ContainerType({
@@ -82,8 +80,7 @@ export type SignableMessage =
82
80
  | {type: SignableMessageType.SYNC_COMMITTEE_MESSAGE; data: ValueOf<typeof SyncCommitteeMessageType>}
83
81
  | {type: SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF; data: ValueOf<typeof SyncAggregatorSelectionDataType>}
84
82
  | {type: SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF; data: altair.ContributionAndProof}
85
- | {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1}
86
- | {type: SignableMessageType.BLS_TO_EXECUTION_CHANGE; data: capella.BLSToExecutionChange};
83
+ | {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1};
87
84
 
88
85
  const requiresForkInfo: Record<SignableMessageType, boolean> = {
89
86
  [SignableMessageType.AGGREGATION_SLOT]: true,
@@ -98,7 +95,6 @@ const requiresForkInfo: Record<SignableMessageType, boolean> = {
98
95
  [SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF]: true,
99
96
  [SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF]: true,
100
97
  [SignableMessageType.VALIDATOR_REGISTRATION]: false,
101
- [SignableMessageType.BLS_TO_EXECUTION_CHANGE]: true,
102
98
  };
103
99
 
104
100
  type Web3SignerSerializedRequest = {
@@ -147,12 +143,12 @@ export async function externalSignerPostSignature(
147
143
  requestObj.signingRoot = toRootHex(signingRoot);
148
144
 
149
145
  if (requiresForkInfo[signableMessage.type]) {
150
- const forkInfo = config.getForkInfo(signingSlot);
146
+ const forkInfo = getForkInfoForSigning(config, signingSlot, signableMessage.type);
151
147
  requestObj.fork_info = {
152
148
  fork: {
153
149
  previous_version: toHex(forkInfo.prevVersion),
154
150
  current_version: toHex(forkInfo.version),
155
- epoch: String(computeEpochAtSlot(signingSlot)),
151
+ epoch: String(forkInfo.epoch),
156
152
  },
157
153
  genesis_validators_root: toRootHex(config.genesisValidatorsRoot),
158
154
  };
@@ -270,8 +266,30 @@ function serializerSignableMessagePayload(config: BeaconConfig, payload: Signabl
270
266
 
271
267
  case SignableMessageType.VALIDATOR_REGISTRATION:
272
268
  return {validator_registration: ssz.bellatrix.ValidatorRegistrationV1.toJson(payload.data)};
269
+ }
270
+ }
273
271
 
274
- case SignableMessageType.BLS_TO_EXECUTION_CHANGE:
275
- return {BLS_TO_EXECUTION_CHANGE: ssz.capella.BLSToExecutionChange.toJson(payload.data)};
272
+ function getForkInfoForSigning(
273
+ config: BeaconConfig,
274
+ signingSlot: Slot,
275
+ messageType: SignableMessageType
276
+ ): {version: Uint8Array; prevVersion: Uint8Array; epoch: number} {
277
+ const forkInfo = config.getForkInfo(signingSlot);
278
+
279
+ if (messageType === SignableMessageType.VOLUNTARY_EXIT && isForkPostDeneb(forkInfo.name)) {
280
+ // Always uses Capella fork post-Deneb (EIP-7044)
281
+ const capellaFork = config.forks[ForkName.capella];
282
+ return {
283
+ version: capellaFork.version,
284
+ prevVersion: capellaFork.prevVersion,
285
+ epoch: capellaFork.epoch,
286
+ };
276
287
  }
288
+
289
+ // Use the fork at the signing slot by default
290
+ return {
291
+ version: forkInfo.version,
292
+ prevVersion: forkInfo.prevVersion,
293
+ epoch: computeEpochAtSlot(signingSlot),
294
+ };
277
295
  }