@lodestar/validator 1.43.0-dev.bc569affb9 → 1.43.0-dev.c5efeb6c90

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 (59) hide show
  1. package/lib/metrics.d.ts +10 -0
  2. package/lib/metrics.d.ts.map +1 -1
  3. package/lib/metrics.js +37 -0
  4. package/lib/metrics.js.map +1 -1
  5. package/lib/services/block.d.ts +2 -1
  6. package/lib/services/block.d.ts.map +1 -1
  7. package/lib/services/block.js +4 -3
  8. package/lib/services/block.js.map +1 -1
  9. package/lib/services/blockDuties.d.ts +85 -7
  10. package/lib/services/blockDuties.d.ts.map +1 -1
  11. package/lib/services/blockDuties.js +186 -74
  12. package/lib/services/blockDuties.js.map +1 -1
  13. package/lib/services/chainHeaderTracker.d.ts +8 -2
  14. package/lib/services/chainHeaderTracker.d.ts.map +1 -1
  15. package/lib/services/chainHeaderTracker.js +23 -8
  16. package/lib/services/chainHeaderTracker.js.map +1 -1
  17. package/lib/services/emitter.d.ts +14 -1
  18. package/lib/services/emitter.d.ts.map +1 -1
  19. package/lib/services/emitter.js +22 -0
  20. package/lib/services/emitter.js.map +1 -1
  21. package/lib/services/proposerPreferences.d.ts +25 -0
  22. package/lib/services/proposerPreferences.d.ts.map +1 -0
  23. package/lib/services/proposerPreferences.js +101 -0
  24. package/lib/services/proposerPreferences.js.map +1 -0
  25. package/lib/services/ptc.d.ts +28 -0
  26. package/lib/services/ptc.d.ts.map +1 -0
  27. package/lib/services/ptc.js +89 -0
  28. package/lib/services/ptc.js.map +1 -0
  29. package/lib/services/ptcDuties.d.ts +31 -0
  30. package/lib/services/ptcDuties.d.ts.map +1 -0
  31. package/lib/services/ptcDuties.js +201 -0
  32. package/lib/services/ptcDuties.js.map +1 -0
  33. package/lib/services/validatorStore.d.ts +3 -0
  34. package/lib/services/validatorStore.d.ts.map +1 -1
  35. package/lib/services/validatorStore.js +54 -1
  36. package/lib/services/validatorStore.js.map +1 -1
  37. package/lib/util/externalSignerClient.d.ts +9 -1
  38. package/lib/util/externalSignerClient.d.ts.map +1 -1
  39. package/lib/util/externalSignerClient.js +8 -0
  40. package/lib/util/externalSignerClient.js.map +1 -1
  41. package/lib/util/params.js +3 -0
  42. package/lib/util/params.js.map +1 -1
  43. package/lib/validator.d.ts +4 -1
  44. package/lib/validator.d.ts.map +1 -1
  45. package/lib/validator.js +13 -3
  46. package/lib/validator.js.map +1 -1
  47. package/package.json +12 -12
  48. package/src/metrics.ts +46 -0
  49. package/src/services/block.ts +3 -9
  50. package/src/services/blockDuties.ts +212 -79
  51. package/src/services/chainHeaderTracker.ts +31 -7
  52. package/src/services/emitter.ts +31 -0
  53. package/src/services/proposerPreferences.ts +124 -0
  54. package/src/services/ptc.ts +131 -0
  55. package/src/services/ptcDuties.ts +246 -0
  56. package/src/services/validatorStore.ts +79 -0
  57. package/src/util/externalSignerClient.ts +13 -1
  58. package/src/util/params.ts +3 -0
  59. package/src/validator.ts +47 -5
@@ -0,0 +1,131 @@
1
+ import {ApiClient, routes} from "@lodestar/api";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {isForkPostGloas} from "@lodestar/params";
4
+ import {Slot, gloas} from "@lodestar/types";
5
+ import {prettyBytes, sleep, toRootHex} from "@lodestar/utils";
6
+ import {Metrics} from "../metrics.js";
7
+ import {PubkeyHex} from "../types.js";
8
+ import {IClock, LoggerVc} from "../util/index.js";
9
+ import {ChainHeaderTracker} from "./chainHeaderTracker.js";
10
+ import {ValidatorEventEmitter} from "./emitter.js";
11
+ import {PtcDutiesService} from "./ptcDuties.js";
12
+ import {SyncingStatusTracker} from "./syncingStatusTracker.js";
13
+ import {ValidatorStore} from "./validatorStore.js";
14
+
15
+ /**
16
+ * Service that sets up and handles validator Payload Timeliness Committee duties.
17
+ */
18
+ export class PtcService {
19
+ private readonly dutiesService: PtcDutiesService;
20
+
21
+ constructor(
22
+ private readonly config: ChainForkConfig,
23
+ private readonly logger: LoggerVc,
24
+ private readonly api: ApiClient,
25
+ private readonly clock: IClock,
26
+ private readonly validatorStore: ValidatorStore,
27
+ private readonly emitter: ValidatorEventEmitter,
28
+ chainHeadTracker: ChainHeaderTracker,
29
+ syncingStatusTracker: SyncingStatusTracker,
30
+ private readonly metrics: Metrics | null
31
+ ) {
32
+ this.dutiesService = new PtcDutiesService(
33
+ config,
34
+ logger,
35
+ api,
36
+ clock,
37
+ validatorStore,
38
+ chainHeadTracker,
39
+ syncingStatusTracker,
40
+ metrics
41
+ );
42
+
43
+ clock.runEverySlot(this.runPtcTasks);
44
+ }
45
+
46
+ removeDutiesForKey(pubkey: PubkeyHex): void {
47
+ this.dutiesService.removeDutiesForKey(pubkey);
48
+ }
49
+
50
+ private runPtcTasks = async (slot: Slot, signal: AbortSignal): Promise<void> => {
51
+ const fork = this.config.getForkName(slot);
52
+ if (!isForkPostGloas(fork)) {
53
+ return;
54
+ }
55
+
56
+ const duties = this.dutiesService.getDutiesAtSlot(slot);
57
+ if (duties.length === 0) {
58
+ return;
59
+ }
60
+
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
+ ]);
66
+
67
+ this.metrics?.ptcStepCallProducePayloadAttestation.observe(
68
+ this.clock.secFromSlot(slot) - payloadAttestationDueMs / 1000
69
+ );
70
+
71
+ try {
72
+ const payloadAttestationData = await this.producePayloadAttestationData(slot);
73
+ await this.signAndPublishPayloadAttestations(slot, payloadAttestationData, duties);
74
+ } catch (e) {
75
+ this.logger.error("Error on PTC routine", {slot}, e as Error);
76
+ }
77
+ };
78
+
79
+ private async producePayloadAttestationData(slot: Slot): Promise<gloas.PayloadAttestationData> {
80
+ return (await this.api.validator.producePayloadAttestationData({slot})).value();
81
+ }
82
+
83
+ private async signAndPublishPayloadAttestations(
84
+ slot: Slot,
85
+ payloadAttestationData: gloas.PayloadAttestationData,
86
+ duties: routes.validator.PtcDuty[]
87
+ ): Promise<void> {
88
+ const payloadAttestationMessages: gloas.PayloadAttestationMessage[] = [];
89
+ const beaconBlockRootHex = toRootHex(payloadAttestationData.beaconBlockRoot);
90
+
91
+ await Promise.all(
92
+ duties.map(async (duty) => {
93
+ const logCtxValidator = {slot, validatorIndex: duty.validatorIndex, beaconBlockRoot: beaconBlockRootHex};
94
+ try {
95
+ payloadAttestationMessages.push(
96
+ await this.validatorStore.signPayloadAttestation(
97
+ duty,
98
+ payloadAttestationData,
99
+ this.clock.getCurrentSlot(),
100
+ this.logger
101
+ )
102
+ );
103
+ this.logger.debug("Signed payload attestation message", logCtxValidator);
104
+ } catch (e) {
105
+ this.metrics?.ptcError.inc({error: "sign"});
106
+ this.logger.error("Error signing payload attestation message", logCtxValidator, e as Error);
107
+ }
108
+ })
109
+ );
110
+
111
+ this.metrics?.ptcStepCallPublishPayloadAttestation.observe(
112
+ this.clock.secFromSlot(slot) -
113
+ this.config.getSlotComponentDurationMs(this.config.PAYLOAD_ATTESTATION_DUE_BPS) / 1000
114
+ );
115
+
116
+ if (payloadAttestationMessages.length > 0) {
117
+ try {
118
+ (await this.api.beacon.submitPayloadAttestationMessages({payloadAttestationMessages})).assertOk();
119
+ this.logger.info("Published payload attestation messages", {
120
+ slot,
121
+ beaconBlockRoot: prettyBytes(beaconBlockRootHex),
122
+ count: payloadAttestationMessages.length,
123
+ });
124
+ this.metrics?.publishedPayloadAttestations.inc(payloadAttestationMessages.length);
125
+ } catch (e) {
126
+ this.metrics?.ptcError.inc({error: "publish"});
127
+ this.logger.error("Error publishing payload attestation messages", {slot}, e as Error);
128
+ }
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,246 @@
1
+ import {ApiClient, routes} from "@lodestar/api";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
4
+ import {computeEpochAtSlot, isStartSlotOfEpoch} from "@lodestar/state-transition";
5
+ import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
6
+ import {toPubkeyHex} from "@lodestar/utils";
7
+ import {Metrics} from "../metrics.js";
8
+ import {PubkeyHex} from "../types.js";
9
+ import {IClock, LoggerVc} from "../util/index.js";
10
+ import {ChainHeaderTracker, HeadEventData} from "./chainHeaderTracker.js";
11
+ import {SyncingStatusTracker} from "./syncingStatusTracker.js";
12
+ import {ValidatorStore} from "./validatorStore.js";
13
+
14
+ /** Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch. */
15
+ const HISTORICAL_DUTIES_EPOCHS = 2;
16
+
17
+ type PtcDutiesAtEpoch = {dependentRoot: RootHex; dutiesByIndex: Map<ValidatorIndex, routes.validator.PtcDuty>};
18
+
19
+ export class PtcDutiesService {
20
+ /** Maps a validator index to its PTC duty for each epoch. */
21
+ private readonly dutiesByIndexByEpoch = new Map<Epoch, PtcDutiesAtEpoch>();
22
+
23
+ constructor(
24
+ private readonly config: ChainForkConfig,
25
+ private readonly logger: LoggerVc,
26
+ private readonly api: ApiClient,
27
+ private readonly clock: IClock,
28
+ private readonly validatorStore: ValidatorStore,
29
+ chainHeadTracker: ChainHeaderTracker,
30
+ syncingStatusTracker: SyncingStatusTracker,
31
+ private readonly metrics: Metrics | null
32
+ ) {
33
+ clock.runEveryEpoch(this.runDutiesTasks);
34
+ chainHeadTracker.runOnNewHead(this.onNewHead);
35
+ syncingStatusTracker.runOnResynced(async (slot) => {
36
+ // Skip on first slot of epoch since tasks are already scheduled.
37
+ if (!isStartSlotOfEpoch(slot)) {
38
+ return this.runDutiesTasks(computeEpochAtSlot(slot));
39
+ }
40
+ });
41
+
42
+ if (metrics) {
43
+ metrics.ptcDutiesCount.addCollect(() => {
44
+ const currentSlot = this.clock.getCurrentSlot();
45
+ let duties = 0;
46
+ let nextDutySlot = null;
47
+ for (const [epoch, ptcDutiesAtEpoch] of this.dutiesByIndexByEpoch) {
48
+ duties += ptcDutiesAtEpoch.dutiesByIndex.size;
49
+
50
+ // Epochs are sorted, stop searching once a next duty slot is found.
51
+ if (epoch < this.clock.currentEpoch || nextDutySlot !== null) continue;
52
+
53
+ for (const duty of ptcDutiesAtEpoch.dutiesByIndex.values()) {
54
+ if (duty.slot > currentSlot && (nextDutySlot === null || duty.slot < nextDutySlot)) {
55
+ nextDutySlot = duty.slot;
56
+ }
57
+ }
58
+ }
59
+ metrics.ptcDutiesCount.set(duties);
60
+ metrics.ptcDutiesEpochCount.set(this.dutiesByIndexByEpoch.size);
61
+ if (nextDutySlot !== null) metrics.ptcDutiesNextSlot.set(nextDutySlot);
62
+ });
63
+ }
64
+ }
65
+
66
+ removeDutiesForKey(pubkey: PubkeyHex): void {
67
+ for (const [epoch, ptcDutiesAtEpoch] of this.dutiesByIndexByEpoch) {
68
+ for (const [validatorIndex, duty] of ptcDutiesAtEpoch.dutiesByIndex) {
69
+ if (toPubkeyHex(duty.pubkey) === pubkey) {
70
+ ptcDutiesAtEpoch.dutiesByIndex.delete(validatorIndex);
71
+ if (ptcDutiesAtEpoch.dutiesByIndex.size === 0) {
72
+ this.dutiesByIndexByEpoch.delete(epoch);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ /** Returns all PTC duties for the given slot. */
80
+ getDutiesAtSlot(slot: Slot): routes.validator.PtcDuty[] {
81
+ const epoch = computeEpochAtSlot(slot);
82
+ const duties: routes.validator.PtcDuty[] = [];
83
+ const epochDuties = this.dutiesByIndexByEpoch.get(epoch);
84
+ if (epochDuties === undefined) {
85
+ return duties;
86
+ }
87
+
88
+ for (const duty of epochDuties.dutiesByIndex.values()) {
89
+ if (duty.slot === slot) {
90
+ duties.push(duty);
91
+ }
92
+ }
93
+
94
+ return duties;
95
+ }
96
+
97
+ private runDutiesTasks = async (epoch: Epoch): Promise<void> => {
98
+ const nextEpoch = epoch + 1;
99
+ if (!isForkPostGloas(this.config.getForkName(nextEpoch * SLOTS_PER_EPOCH))) {
100
+ return;
101
+ }
102
+
103
+ await Promise.all([
104
+ this.pollPtcDuties(epoch, this.validatorStore.getAllLocalIndices()).catch((e: Error) => {
105
+ this.logger.error("Error on poll PTC duties", {epoch}, e);
106
+ }),
107
+
108
+ this.validatorStore
109
+ .pollValidatorIndices()
110
+ .then((newIndices) => this.pollPtcDuties(epoch, newIndices))
111
+ .catch((e: Error) => {
112
+ this.logger.error("Error on poll indices and PTC duties", {epoch}, e);
113
+ }),
114
+ ]);
115
+
116
+ this.pruneOldDuties(epoch);
117
+ };
118
+
119
+ private async pollPtcDuties(currentEpoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
120
+ const nextEpoch = currentEpoch + 1;
121
+
122
+ if (indexArr.length === 0) {
123
+ return;
124
+ }
125
+
126
+ for (const epoch of [currentEpoch, nextEpoch]) {
127
+ await this.pollPtcDutiesForEpoch(epoch, indexArr).catch((e: Error) => {
128
+ this.logger.error("Failed to download PTC duties", {epoch}, e);
129
+ });
130
+ }
131
+ }
132
+
133
+ private async pollPtcDutiesForEpoch(epoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
134
+ if (epoch < 0) {
135
+ return;
136
+ }
137
+
138
+ if (!isForkPostGloas(this.config.getForkName(epoch * SLOTS_PER_EPOCH))) {
139
+ return;
140
+ }
141
+
142
+ const res = await this.api.validator.getPtcDuties({epoch, indices: indexArr});
143
+ const ptcDuties = res.value();
144
+ const {dependentRoot} = res.meta();
145
+ const relevantDuties = ptcDuties.filter((duty) => {
146
+ const pubkeyHex = toPubkeyHex(duty.pubkey);
147
+ return this.validatorStore.hasVotingPubkey(pubkeyHex) && this.validatorStore.isDoppelgangerSafe(pubkeyHex);
148
+ });
149
+
150
+ this.logger.debug("Downloaded PTC duties", {epoch, dependentRoot, count: relevantDuties.length});
151
+
152
+ const dutiesAtEpoch = this.dutiesByIndexByEpoch.get(epoch);
153
+ const priorDependentRoot = dutiesAtEpoch?.dependentRoot;
154
+ const dependentRootChanged = priorDependentRoot !== undefined && priorDependentRoot !== dependentRoot;
155
+
156
+ if (!priorDependentRoot || dependentRootChanged) {
157
+ const dutiesByIndex = new Map<ValidatorIndex, routes.validator.PtcDuty>();
158
+ for (const duty of relevantDuties) {
159
+ dutiesByIndex.set(duty.validatorIndex, duty);
160
+ }
161
+ this.dutiesByIndexByEpoch.set(epoch, {dependentRoot, dutiesByIndex});
162
+
163
+ if (priorDependentRoot && dependentRootChanged) {
164
+ this.metrics?.ptcDutiesReorg.inc();
165
+ this.logger.warn("PTC duties re-org. This may happen from time to time", {
166
+ priorDependentRoot,
167
+ dependentRoot,
168
+ epoch,
169
+ });
170
+ }
171
+ } else {
172
+ const existingDuties = dutiesAtEpoch.dutiesByIndex;
173
+ const existingDutiesCount = existingDuties.size;
174
+ const discoveredNewDuties = relevantDuties.length > existingDutiesCount;
175
+
176
+ if (discoveredNewDuties) {
177
+ for (const duty of relevantDuties) {
178
+ if (!existingDuties.has(duty.validatorIndex)) {
179
+ existingDuties.set(duty.validatorIndex, duty);
180
+ }
181
+ }
182
+
183
+ this.logger.debug("Discovered new PTC duties", {
184
+ epoch,
185
+ dependentRoot,
186
+ count: relevantDuties.length - existingDutiesCount,
187
+ });
188
+ }
189
+ }
190
+ }
191
+
192
+ private onNewHead = async ({
193
+ slot,
194
+ previousDutyDependentRoot,
195
+ currentDutyDependentRoot,
196
+ }: HeadEventData): Promise<void> => {
197
+ const currentEpoch = computeEpochAtSlot(slot);
198
+ const nextEpoch = currentEpoch + 1;
199
+
200
+ const nextEpochDependentRoot = this.dutiesByIndexByEpoch.get(nextEpoch)?.dependentRoot;
201
+ if (nextEpochDependentRoot && currentDutyDependentRoot !== nextEpochDependentRoot) {
202
+ this.logger.warn("Potential next epoch PTC duties reorg", {
203
+ slot,
204
+ dutyEpoch: nextEpoch,
205
+ priorDependentRoot: nextEpochDependentRoot,
206
+ newDependentRoot: currentDutyDependentRoot,
207
+ });
208
+ await this.handlePtcDutiesReorg(nextEpoch, slot, nextEpochDependentRoot, currentDutyDependentRoot);
209
+ }
210
+
211
+ const currentEpochDependentRoot = this.dutiesByIndexByEpoch.get(currentEpoch)?.dependentRoot;
212
+ if (currentEpochDependentRoot && currentEpochDependentRoot !== previousDutyDependentRoot) {
213
+ this.logger.warn("Potential current epoch PTC duties reorg", {
214
+ slot,
215
+ dutyEpoch: currentEpoch,
216
+ priorDependentRoot: currentEpochDependentRoot,
217
+ newDependentRoot: previousDutyDependentRoot,
218
+ });
219
+ await this.handlePtcDutiesReorg(currentEpoch, slot, currentEpochDependentRoot, previousDutyDependentRoot);
220
+ }
221
+ };
222
+
223
+ private async handlePtcDutiesReorg(
224
+ dutyEpoch: Epoch,
225
+ slot: Slot,
226
+ oldDependentRoot: RootHex,
227
+ newDependentRoot: RootHex
228
+ ): Promise<void> {
229
+ this.metrics?.ptcDutiesReorg.inc();
230
+ const logContext = {dutyEpoch, slot, oldDependentRoot, newDependentRoot};
231
+ this.logger.debug("Redownload PTC duties", logContext);
232
+
233
+ await this.pollPtcDutiesForEpoch(dutyEpoch, this.validatorStore.getAllLocalIndices()).catch((e: Error) => {
234
+ this.logger.error("Failed to redownload PTC duties when reorg happens", logContext, e);
235
+ });
236
+ }
237
+
238
+ /** Run once per epoch to prune duties map. */
239
+ private pruneOldDuties(currentEpoch: Epoch): void {
240
+ for (const epoch of this.dutiesByIndexByEpoch.keys()) {
241
+ if (epoch + HISTORICAL_DUTIES_EPOCHS < currentEpoch) {
242
+ this.dutiesByIndexByEpoch.delete(epoch);
243
+ }
244
+ }
245
+ }
246
+ }
@@ -9,6 +9,8 @@ import {
9
9
  DOMAIN_BEACON_BUILDER,
10
10
  DOMAIN_BEACON_PROPOSER,
11
11
  DOMAIN_CONTRIBUTION_AND_PROOF,
12
+ DOMAIN_PROPOSER_PREFERENCES,
13
+ DOMAIN_PTC_ATTESTER,
12
14
  DOMAIN_RANDAO,
13
15
  DOMAIN_SELECTION_PROOF,
14
16
  DOMAIN_SYNC_COMMITTEE,
@@ -668,6 +670,77 @@ export class ValidatorStore {
668
670
  };
669
671
  }
670
672
 
673
+ async signPayloadAttestation(
674
+ duty: routes.validator.PtcDuty,
675
+ data: gloas.PayloadAttestationData,
676
+ currentSlot: Slot,
677
+ logger?: LoggerVc
678
+ ): Promise<gloas.PayloadAttestationMessage> {
679
+ if (data.slot > currentSlot) {
680
+ throw Error(`Not signing payload attestation with slot ${data.slot} greater than current slot ${currentSlot}`);
681
+ }
682
+
683
+ this.assertDoppelgangerSafe(duty.pubkey);
684
+ this.validatePtcDuty(duty, data);
685
+
686
+ const signingSlot = data.slot;
687
+ const domain = this.config.getDomain(signingSlot, DOMAIN_PTC_ATTESTER);
688
+ const signingRoot = computeSigningRoot(ssz.gloas.PayloadAttestationData, data, domain);
689
+
690
+ logger?.debug("Signing payload attestation message", {
691
+ slot: signingSlot,
692
+ beaconBlockRoot: toRootHex(data.beaconBlockRoot),
693
+ signingRoot: toRootHex(signingRoot),
694
+ });
695
+
696
+ const signableMessage: SignableMessage = {
697
+ type: SignableMessageType.PAYLOAD_ATTESTATION,
698
+ data,
699
+ };
700
+
701
+ return {
702
+ validatorIndex: duty.validatorIndex,
703
+ data,
704
+ signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
705
+ };
706
+ }
707
+
708
+ async signProposerPreferences(
709
+ duty: routes.validator.ProposerDuty,
710
+ dependentRoot: Uint8Array,
711
+ feeRecipient: ExecutionAddress,
712
+ gasLimit: number,
713
+ currentSlot: Slot
714
+ ): Promise<gloas.SignedProposerPreferences> {
715
+ if (duty.slot <= currentSlot) {
716
+ throw Error(`Not signing proposer preferences for past slot ${duty.slot} (current ${currentSlot})`);
717
+ }
718
+
719
+ this.assertDoppelgangerSafe(duty.pubkey);
720
+
721
+ const message: gloas.ProposerPreferences = {
722
+ dependentRoot,
723
+ proposalSlot: duty.slot,
724
+ validatorIndex: duty.validatorIndex,
725
+ feeRecipient: fromHex(feeRecipient),
726
+ gasLimit,
727
+ };
728
+
729
+ const signingSlot = duty.slot;
730
+ const domain = this.config.getDomain(signingSlot, DOMAIN_PROPOSER_PREFERENCES);
731
+ const signingRoot = computeSigningRoot(ssz.gloas.ProposerPreferences, message, domain);
732
+
733
+ const signableMessage: SignableMessage = {
734
+ type: SignableMessageType.PROPOSER_PREFERENCES,
735
+ data: message,
736
+ };
737
+
738
+ return {
739
+ message,
740
+ signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
741
+ };
742
+ }
743
+
671
744
  async signAttestationSelectionProof(pubkey: BLSPubkeyMaybeHex, slot: Slot): Promise<BLSSignature> {
672
745
  const signingSlot = slot;
673
746
  const domain = this.config.getDomain(slot, DOMAIN_SELECTION_PROOF);
@@ -852,6 +925,12 @@ export class ValidatorStore {
852
925
  }
853
926
  }
854
927
 
928
+ private validatePtcDuty(duty: routes.validator.PtcDuty, data: gloas.PayloadAttestationData): void {
929
+ if (duty.slot !== data.slot) {
930
+ throw Error(`Inconsistent PTC duties during signing: duty.slot ${duty.slot} != data.slot ${data.slot}`);
931
+ }
932
+ }
933
+
855
934
  private assertDoppelgangerSafe(pubKey: PubkeyHex | BLSPubkey): void {
856
935
  const pubkeyHex = typeof pubKey === "string" ? pubKey : toPubkeyHex(pubKey);
857
936
  if (!this.isDoppelgangerSafe(pubkeyHex)) {
@@ -34,6 +34,8 @@ export enum SignableMessageType {
34
34
  SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF = "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF",
35
35
  VALIDATOR_REGISTRATION = "VALIDATOR_REGISTRATION",
36
36
  EXECUTION_PAYLOAD_ENVELOPE = "EXECUTION_PAYLOAD_ENVELOPE",
37
+ PAYLOAD_ATTESTATION = "PAYLOAD_ATTESTATION",
38
+ PROPOSER_PREFERENCES = "PROPOSER_PREFERENCES",
37
39
  }
38
40
 
39
41
  const AggregationSlotType = new ContainerType({
@@ -83,7 +85,9 @@ export type SignableMessage =
83
85
  | {type: SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF; data: ValueOf<typeof SyncAggregatorSelectionDataType>}
84
86
  | {type: SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF; data: altair.ContributionAndProof}
85
87
  | {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1}
86
- | {type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE; data: gloas.ExecutionPayloadEnvelope};
88
+ | {type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE; data: gloas.ExecutionPayloadEnvelope}
89
+ | {type: SignableMessageType.PAYLOAD_ATTESTATION; data: gloas.PayloadAttestationData}
90
+ | {type: SignableMessageType.PROPOSER_PREFERENCES; data: gloas.ProposerPreferences};
87
91
 
88
92
  const requiresForkInfo: Record<SignableMessageType, boolean> = {
89
93
  [SignableMessageType.AGGREGATION_SLOT]: true,
@@ -99,6 +103,8 @@ const requiresForkInfo: Record<SignableMessageType, boolean> = {
99
103
  [SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF]: true,
100
104
  [SignableMessageType.VALIDATOR_REGISTRATION]: false,
101
105
  [SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE]: true,
106
+ [SignableMessageType.PAYLOAD_ATTESTATION]: true,
107
+ [SignableMessageType.PROPOSER_PREFERENCES]: true,
102
108
  };
103
109
 
104
110
  type Web3SignerSerializedRequest = {
@@ -273,6 +279,12 @@ function serializerSignableMessagePayload(config: BeaconConfig, payload: Signabl
273
279
 
274
280
  case SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE:
275
281
  return {execution_payload_envelope: ssz.gloas.ExecutionPayloadEnvelope.toJson(payload.data)};
282
+
283
+ case SignableMessageType.PAYLOAD_ATTESTATION:
284
+ return {payload_attestation: ssz.gloas.PayloadAttestationData.toJson(payload.data)};
285
+
286
+ case SignableMessageType.PROPOSER_PREFERENCES:
287
+ return {proposer_preferences: ssz.gloas.ProposerPreferences.toJson(payload.data)};
276
288
  }
277
289
  }
278
290
 
@@ -327,5 +327,8 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record<keyof ConfigWit
327
327
  BUILDER_PENDING_WITHDRAWALS_LIMIT: gloasForkRelevant,
328
328
  MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: gloasForkRelevant,
329
329
  MIN_BUILDER_WITHDRAWABILITY_DELAY: gloasForkRelevant,
330
+ CHURN_LIMIT_QUOTIENT_GLOAS: gloasForkRelevant,
331
+ CONSOLIDATION_CHURN_LIMIT_QUOTIENT: gloasForkRelevant,
332
+ MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS: gloasForkRelevant,
330
333
  };
331
334
  }
package/src/validator.ts CHANGED
@@ -9,12 +9,15 @@ import {Metrics} from "./metrics.js";
9
9
  import {MetaDataRepository} from "./repositories/metaDataRepository.js";
10
10
  import {AttestationService} from "./services/attestation.js";
11
11
  import {BlockProposingService} from "./services/block.js";
12
+ import {BlockDutiesService} from "./services/blockDuties.js";
12
13
  import {ChainHeaderTracker} from "./services/chainHeaderTracker.js";
13
14
  import {DoppelgangerService} from "./services/doppelgangerService.js";
14
15
  import {ValidatorEventEmitter} from "./services/emitter.js";
15
16
  import {ExternalSignerOptions, pollExternalSignerPubkeys} from "./services/externalSignerSync.js";
16
17
  import {IndicesService} from "./services/indices.js";
17
18
  import {pollBuilderValidatorRegistration, pollPrepareBeaconProposer} from "./services/prepareBeaconProposer.js";
19
+ import {ProposerPreferencesService} from "./services/proposerPreferences.js";
20
+ import {PtcService} from "./services/ptc.js";
18
21
  import {SyncCommitteeService} from "./services/syncCommittee.js";
19
22
  import {SyncingStatusTracker} from "./services/syncingStatusTracker.js";
20
23
  import {Signer, ValidatorProposerConfig, ValidatorStore, defaultOptions} from "./services/validatorStore.js";
@@ -30,6 +33,7 @@ export type ValidatorModules = {
30
33
  slashingProtection: ISlashingProtection;
31
34
  blockProposingService: BlockProposingService;
32
35
  attestationService: AttestationService;
36
+ ptcService: PtcService;
33
37
  syncCommitteeService: SyncCommitteeService;
34
38
  config: BeaconConfig;
35
39
  api: ApiClient;
@@ -84,6 +88,7 @@ export class Validator {
84
88
  private readonly slashingProtection: ISlashingProtection;
85
89
  private readonly blockProposingService: BlockProposingService;
86
90
  private readonly attestationService: AttestationService;
91
+ private readonly ptcService: PtcService;
87
92
  private readonly syncCommitteeService: SyncCommitteeService;
88
93
  private readonly config: BeaconConfig;
89
94
  private readonly api: ApiClient;
@@ -102,6 +107,7 @@ export class Validator {
102
107
  slashingProtection,
103
108
  blockProposingService,
104
109
  attestationService,
110
+ ptcService,
105
111
  syncCommitteeService,
106
112
  config,
107
113
  api,
@@ -118,6 +124,7 @@ export class Validator {
118
124
  this.slashingProtection = slashingProtection;
119
125
  this.blockProposingService = blockProposingService;
120
126
  this.attestationService = attestationService;
127
+ this.ptcService = ptcService;
121
128
  this.syncCommitteeService = syncCommitteeService;
122
129
  this.config = config;
123
130
  this.api = api;
@@ -225,13 +232,32 @@ export class Validator {
225
232
  // We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10
226
233
  emitter.setMaxListeners(Infinity);
227
234
 
228
- const chainHeaderTracker = new ChainHeaderTracker(logger, api, emitter);
235
+ const chainHeaderTracker = new ChainHeaderTracker(config, logger, api, emitter);
229
236
  const syncingStatusTracker = new SyncingStatusTracker(logger, api, clock, metrics);
230
237
 
231
- const blockProposingService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, metrics, {
232
- broadcastValidation: opts.broadcastValidation ?? defaultOptions.broadcastValidation,
233
- blindedLocal: opts.blindedLocal ?? defaultOptions.blindedLocal,
234
- });
238
+ const blockDutiesService = new BlockDutiesService(
239
+ config,
240
+ loggerVc,
241
+ api,
242
+ clock,
243
+ validatorStore,
244
+ chainHeaderTracker,
245
+ metrics
246
+ );
247
+
248
+ const blockProposingService = new BlockProposingService(
249
+ config,
250
+ loggerVc,
251
+ api,
252
+ clock,
253
+ validatorStore,
254
+ blockDutiesService,
255
+ metrics,
256
+ {
257
+ broadcastValidation: opts.broadcastValidation ?? defaultOptions.broadcastValidation,
258
+ blindedLocal: opts.blindedLocal ?? defaultOptions.blindedLocal,
259
+ }
260
+ );
235
261
 
236
262
  const attestationService = new AttestationService(
237
263
  loggerVc,
@@ -249,6 +275,18 @@ export class Validator {
249
275
  }
250
276
  );
251
277
 
278
+ const ptcService = new PtcService(
279
+ config,
280
+ loggerVc,
281
+ api,
282
+ clock,
283
+ validatorStore,
284
+ emitter,
285
+ chainHeaderTracker,
286
+ syncingStatusTracker,
287
+ metrics
288
+ );
289
+
252
290
  const syncCommitteeService = new SyncCommitteeService(
253
291
  config,
254
292
  loggerVc,
@@ -265,6 +303,8 @@ export class Validator {
265
303
  }
266
304
  );
267
305
 
306
+ new ProposerPreferencesService(config, loggerVc, api, clock, validatorStore, blockDutiesService, metrics);
307
+
268
308
  return new Validator({
269
309
  opts,
270
310
  genesis,
@@ -272,6 +312,7 @@ export class Validator {
272
312
  slashingProtection,
273
313
  blockProposingService,
274
314
  attestationService,
315
+ ptcService,
275
316
  syncCommitteeService,
276
317
  config,
277
318
  api,
@@ -338,6 +379,7 @@ export class Validator {
338
379
  removeDutiesForKey(pubkey: PubkeyHex): void {
339
380
  this.blockProposingService.removeDutiesForKey(pubkey);
340
381
  this.attestationService.removeDutiesForKey(pubkey);
382
+ this.ptcService.removeDutiesForKey(pubkey);
341
383
  this.syncCommitteeService.removeDutiesForKey(pubkey);
342
384
  }
343
385