@lodestar/validator 1.51.0-dev.4fa9ea1184 → 1.51.0-dev.5e25b73306

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 (35) hide show
  1. package/lib/services/chainHeaderTracker.d.ts.map +1 -1
  2. package/lib/services/chainHeaderTracker.js +6 -1
  3. package/lib/services/chainHeaderTracker.js.map +1 -1
  4. package/lib/services/emitter.d.ts +10 -1
  5. package/lib/services/emitter.d.ts.map +1 -1
  6. package/lib/services/emitter.js +4 -0
  7. package/lib/services/emitter.js.map +1 -1
  8. package/lib/services/inclusionList.d.ts +25 -0
  9. package/lib/services/inclusionList.d.ts.map +1 -0
  10. package/lib/services/inclusionList.js +101 -0
  11. package/lib/services/inclusionList.js.map +1 -0
  12. package/lib/services/inclusionListDuties.d.ts +59 -0
  13. package/lib/services/inclusionListDuties.d.ts.map +1 -0
  14. package/lib/services/inclusionListDuties.js +252 -0
  15. package/lib/services/inclusionListDuties.js.map +1 -0
  16. package/lib/services/validatorStore.d.ts +7 -1
  17. package/lib/services/validatorStore.d.ts.map +1 -1
  18. package/lib/services/validatorStore.js +26 -1
  19. package/lib/services/validatorStore.js.map +1 -1
  20. package/lib/util/externalSignerClient.d.ts +6 -2
  21. package/lib/util/externalSignerClient.d.ts.map +1 -1
  22. package/lib/util/externalSignerClient.js +4 -0
  23. package/lib/util/externalSignerClient.js.map +1 -1
  24. package/lib/validator.d.ts +4 -1
  25. package/lib/validator.d.ts.map +1 -1
  26. package/lib/validator.js +7 -1
  27. package/lib/validator.js.map +1 -1
  28. package/package.json +12 -12
  29. package/src/services/chainHeaderTracker.ts +8 -1
  30. package/src/services/emitter.ts +10 -0
  31. package/src/services/inclusionList.ts +140 -0
  32. package/src/services/inclusionListDuties.ts +306 -0
  33. package/src/services/validatorStore.ts +39 -0
  34. package/src/util/externalSignerClient.ts +8 -1
  35. package/src/validator.ts +18 -0
@@ -0,0 +1,140 @@
1
+ import {ApiClient, routes} from "@lodestar/api";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {IClock} from "@lodestar/state-transition";
4
+ import {Slot, bellatrix, heze} from "@lodestar/types";
5
+ import {sleep} from "@lodestar/utils";
6
+ import {PubkeyHex} from "../types.js";
7
+ import {LoggerVc} from "../util/index.js";
8
+ import {ChainHeaderTracker} from "./chainHeaderTracker.js";
9
+ import {ExecutionPayloadImportedEventData, ValidatorEvent, ValidatorEventEmitter} from "./emitter.js";
10
+ import {InclusionListDutiesService} from "./inclusionListDuties.js";
11
+ import {SyncingStatusTracker} from "./syncingStatusTracker.js";
12
+ import {ValidatorStore} from "./validatorStore.js";
13
+
14
+ export class InclusionListService {
15
+ private readonly dutiesService: InclusionListDutiesService;
16
+
17
+ constructor(
18
+ private readonly config: ChainForkConfig,
19
+ private readonly logger: LoggerVc,
20
+ private readonly api: ApiClient,
21
+ private readonly clock: IClock,
22
+ private readonly validatorStore: ValidatorStore,
23
+ chainHeadTracker: ChainHeaderTracker,
24
+ syncingStatusTracker: SyncingStatusTracker,
25
+ private readonly emitter: ValidatorEventEmitter
26
+ ) {
27
+ this.dutiesService = new InclusionListDutiesService(
28
+ config,
29
+ logger,
30
+ api,
31
+ clock,
32
+ validatorStore,
33
+ chainHeadTracker,
34
+ syncingStatusTracker
35
+ );
36
+
37
+ // At most every slot, check existing duties from InclusionListDutiesService and run tasks
38
+ clock.runEverySlot(this.runInclusionListTasks);
39
+ }
40
+
41
+ removeDutiesForKey(pubkey: PubkeyHex): void {
42
+ this.dutiesService.removeDutiesForKey(pubkey);
43
+ }
44
+
45
+ private runInclusionListTasks = async (slot: Slot, signal: AbortSignal): Promise<void> => {
46
+ if (this.dutiesService.getDutiesAtSlot(slot).length === 0) {
47
+ return;
48
+ }
49
+ // Spec: broadcast by get_inclusion_list_due_ms(). Wait until the beacon node imported the slot's payload,
50
+ // emitted once the execution client verified it and the fork choice update was queued, so the list is built
51
+ // from a mempool without that payload's transactions. Fall back to the deadline for empty or late slots.
52
+ const dueMs = Math.max(
53
+ 0,
54
+ this.config.getInclusionListDueMs(this.config.getForkName(slot)) - this.clock.msFromSlot(slot)
55
+ );
56
+ // Publish ahead of the deadline so the list still counts as timely for peers
57
+ const beforeDueMs = 1000;
58
+ let resolvePayloadImported = (): void => {};
59
+ const payloadImported = new Promise<void>((resolve) => {
60
+ resolvePayloadImported = resolve;
61
+ });
62
+ const onPayloadImported = (payload: ExecutionPayloadImportedEventData): void => {
63
+ if (payload.slot === slot) {
64
+ resolvePayloadImported();
65
+ }
66
+ };
67
+ this.emitter.on(ValidatorEvent.executionPayloadImported, onPayloadImported);
68
+ try {
69
+ await Promise.race([sleep(Math.max(0, dueMs - beforeDueMs), signal), payloadImported]);
70
+ } finally {
71
+ this.emitter.off(ValidatorEvent.executionPayloadImported, onPayloadImported);
72
+ }
73
+
74
+ const inclusionListTransactions = await this.produceInclusionList(slot);
75
+
76
+ // Empty lists are ignored by gossip validation (consensus-specs#5576)
77
+ if (inclusionListTransactions.length === 0) {
78
+ this.logger.debug("Produced inclusion list has no transactions, skipping publish", {slot});
79
+ return;
80
+ }
81
+
82
+ // Read the duties after the wait, a reorg handled meanwhile may have changed the dependent root
83
+ const duties = this.dutiesService.getDutiesAtSlot(slot);
84
+ if (duties.length === 0) {
85
+ return;
86
+ }
87
+
88
+ await this.signAndPublishInclusionList(inclusionListTransactions, duties);
89
+ };
90
+
91
+ private async produceInclusionList(slot: Slot): Promise<bellatrix.Transactions> {
92
+ return (await this.api.validator.produceInclusionList({slot})).value();
93
+ }
94
+
95
+ /** One inclusion list per slot, signed by every validator on duty */
96
+ private async signAndPublishInclusionList(
97
+ inclusionListTransactions: bellatrix.Transactions,
98
+ duties: routes.validator.InclusionListDutyList
99
+ ) {
100
+ const signedInclusionLists: heze.SignedInclusionList[] = [];
101
+
102
+ await Promise.all(
103
+ duties.map(async (duty) => {
104
+ const inclusionList: heze.InclusionList = {
105
+ slot: duty.slot,
106
+ validatorIndex: duty.validatorIndex,
107
+ dependentRoot: duty.dependentRoot,
108
+ transactions: inclusionListTransactions,
109
+ };
110
+ try {
111
+ signedInclusionLists.push(await this.validatorStore.signInclusionList(duty, inclusionList));
112
+ } catch (e) {
113
+ this.logger.error(
114
+ "Error signing inclusion list",
115
+ {slot: duty.slot, validatorIndex: duty.validatorIndex},
116
+ e as Error
117
+ );
118
+ }
119
+ })
120
+ );
121
+
122
+ for (const signedInclusionList of signedInclusionLists) {
123
+ const {slot, validatorIndex, transactions} = signedInclusionList.message;
124
+ try {
125
+ (await this.api.validator.publishInclusionList({signedInclusionList})).assertOk();
126
+ this.logger.info("Published inclusionList", {
127
+ slot,
128
+ validatorIndex,
129
+ transactions: transactions.length,
130
+ });
131
+ } catch (e) {
132
+ this.logger.error(
133
+ "Error publishing inclusionList",
134
+ {slot, validatorIndex, transactions: transactions.length},
135
+ e as Error
136
+ );
137
+ }
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,306 @@
1
+ import {ApiClient, routes} from "@lodestar/api";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {SLOTS_PER_EPOCH} from "@lodestar/params";
4
+ import {IClock, computeEpochAtSlot, isStartSlotOfEpoch} from "@lodestar/state-transition";
5
+ import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
6
+ import {sleep, toPubkeyHex} from "@lodestar/utils";
7
+ import {PubkeyHex} from "../types.js";
8
+ import {LoggerVc} from "../util/index.js";
9
+ import {ChainHeaderTracker, HeadEventData} from "./chainHeaderTracker.js";
10
+ import {SyncingStatusTracker} from "./syncingStatusTracker.js";
11
+ import {ValidatorStore} from "./validatorStore.js";
12
+
13
+ /** Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch. */
14
+ const HISTORICAL_DUTIES_EPOCHS = 2;
15
+
16
+ type InclusionListDuty = routes.validator.InclusionListDuty;
17
+ // To assist with readability
18
+ type InclusionListDutiesAtEpoch = {dependentRoot: RootHex; dutiesByIndex: Map<ValidatorIndex, InclusionListDuty>};
19
+
20
+ /**
21
+ * Similar to AttestationDutiesService but
22
+ * - No generating and caching selection proof
23
+ * - No handling and maintaining subnet subscription
24
+ */
25
+ export class InclusionListDutiesService {
26
+ /** Maps a validator index to its duty for each epoch */
27
+ private readonly dutiesByIndexByEpoch = new Map<Epoch, InclusionListDutiesAtEpoch>();
28
+ /**
29
+ * We may receive new dependentRoot of an epoch but it's not the last slot of epoch
30
+ * so we have to wait for getting close to the next epoch to redownload new inclusionListDuties.
31
+ */
32
+ private readonly pendingDependentRootByEpoch = new Map<Epoch, RootHex>();
33
+
34
+ constructor(
35
+ private readonly config: ChainForkConfig,
36
+ private readonly logger: LoggerVc,
37
+ private readonly api: ApiClient,
38
+ private clock: IClock,
39
+ private readonly validatorStore: ValidatorStore,
40
+ chainHeadTracker: ChainHeaderTracker,
41
+ syncingStatusTracker: SyncingStatusTracker
42
+ ) {
43
+ // Running this task every epoch is safe since a re-org of two epochs is very unlikely
44
+ clock.runEveryEpoch(this.runDutiesTasks);
45
+ clock.runEverySlot(this.prepareForNextEpoch);
46
+ chainHeadTracker.runOnNewHead(this.onNewHead);
47
+ syncingStatusTracker.runOnResynced(async (slot) => {
48
+ // Skip on first slot of epoch since tasks are already scheduled
49
+ if (!isStartSlotOfEpoch(slot)) {
50
+ return this.runDutiesTasks(computeEpochAtSlot(slot));
51
+ }
52
+ });
53
+ }
54
+
55
+ removeDutiesForKey(pubkey: PubkeyHex): void {
56
+ for (const [epoch, dutiesAtEpoch] of this.dutiesByIndexByEpoch) {
57
+ for (const [vIndex, duty] of dutiesAtEpoch.dutiesByIndex) {
58
+ if (toPubkeyHex(duty.pubkey) === pubkey) {
59
+ dutiesAtEpoch.dutiesByIndex.delete(vIndex);
60
+ if (dutiesAtEpoch.dutiesByIndex.size === 0) {
61
+ this.dutiesByIndexByEpoch.delete(epoch);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ /** Returns all `ValidatorDuty` for the given `slot` */
69
+ getDutiesAtSlot(slot: Slot): InclusionListDuty[] {
70
+ const epoch = computeEpochAtSlot(slot);
71
+ const duties: InclusionListDuty[] = [];
72
+ const epochDuties = this.dutiesByIndexByEpoch.get(epoch);
73
+ if (epochDuties === undefined) {
74
+ return duties;
75
+ }
76
+
77
+ for (const validatorDuty of epochDuties.dutiesByIndex.values()) {
78
+ if (validatorDuty.slot === slot) {
79
+ duties.push(validatorDuty);
80
+ }
81
+ }
82
+
83
+ return duties;
84
+ }
85
+
86
+ /**
87
+ * If a reorg dependent root comes at a slot other than last slot of epoch
88
+ * just update this.pendingDependentRootByEpoch() and process here
89
+ */
90
+ private prepareForNextEpoch = async (slot: Slot, signal: AbortSignal): Promise<void> => {
91
+ // only interested in last slot of epoch
92
+ if ((slot + 1) % SLOTS_PER_EPOCH !== 0) {
93
+ return;
94
+ }
95
+
96
+ // during the 1 / 3 of epoch, last block of epoch may come
97
+ await sleep(this.clock.msToSlot(slot + 1 / 3), signal);
98
+
99
+ const nextEpoch = computeEpochAtSlot(slot) + 1;
100
+ const dependentRoot = this.dutiesByIndexByEpoch.get(nextEpoch)?.dependentRoot;
101
+ const pendingDependentRoot = this.pendingDependentRootByEpoch.get(nextEpoch);
102
+ if (dependentRoot && pendingDependentRoot && dependentRoot !== pendingDependentRoot) {
103
+ // this happens when pendingDependentRoot is not the last block of an epoch
104
+ this.logger.info("Redownload inclusion list duties when it's close to epoch boundary", {nextEpoch, slot});
105
+ await this.handleInclusionListDutiesReorg(nextEpoch, slot, dependentRoot, pendingDependentRoot);
106
+ }
107
+ };
108
+
109
+ private runDutiesTasks = async (epoch: Epoch): Promise<void> => {
110
+ // pollInclusionListCommittee also polls epoch + 1, so start one epoch before the fork
111
+ if (epoch + 1 < this.config.HEZE_FORK_EPOCH) {
112
+ return;
113
+ }
114
+
115
+ await Promise.all([
116
+ // Run pollInclusionListCommittee immediately for all known local indices
117
+ this.pollInclusionListCommittee(epoch, this.validatorStore.getAllLocalIndices()).catch((e: Error) => {
118
+ this.logger.error("Error on poll inclusion list committee", {epoch}, e);
119
+ }),
120
+
121
+ // At the same time fetch any remaining unknown validator indices, then poll duties for those newIndices only
122
+ this.validatorStore
123
+ .pollValidatorIndices()
124
+ .then((newIndices) => this.pollInclusionListCommittee(epoch, newIndices))
125
+ .catch((e: Error) => {
126
+ this.logger.error("Error on poll indices and inclusion list committee", {epoch}, e);
127
+ }),
128
+ ]);
129
+
130
+ // After both, prune
131
+ this.pruneOldDuties(epoch);
132
+ };
133
+
134
+ private async pollInclusionListCommittee(currentEpoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
135
+ const nextEpoch = currentEpoch + 1;
136
+
137
+ // No need to bother the BN if we don't have any validators.
138
+ if (indexArr.length === 0) {
139
+ return;
140
+ }
141
+
142
+ for (const epoch of [currentEpoch, nextEpoch]) {
143
+ // Download the duties and update the duties for the current and next epoch.
144
+ await this.pollInclusionListCommitteeForEpoch(epoch, indexArr).catch((e: Error) => {
145
+ this.logger.error("Failed to download inclusion list duties", {epoch}, e);
146
+ });
147
+ }
148
+ }
149
+
150
+ /**
151
+ * For the given `indexArr`, download the duties for the given `epoch` and store them in duties.
152
+ */
153
+ private async pollInclusionListCommitteeForEpoch(epoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
154
+ // Don't fetch duties for epochs before genesis. However, should fetch epoch 0 duties at epoch -1
155
+ if (epoch < 0) {
156
+ return;
157
+ }
158
+
159
+ // Inclusion list committees only exist post-heze; the beacon node rejects earlier epochs
160
+ if (epoch < this.config.HEZE_FORK_EPOCH) {
161
+ return;
162
+ }
163
+
164
+ const res = await this.api.validator.getInclusionListCommitteeDuties({epoch, indices: indexArr});
165
+ const inclusionListDuties = res.value();
166
+ const {dependentRoot} = res.meta();
167
+ const relevantDuties = inclusionListDuties.filter((duty) => {
168
+ const pubkeyHex = toPubkeyHex(duty.pubkey);
169
+ return this.validatorStore.hasVotingPubkey(pubkeyHex) && this.validatorStore.isDoppelgangerSafe(pubkeyHex);
170
+ });
171
+
172
+ this.logger.debug("Downloaded inclusion list duties", {epoch, dependentRoot, count: relevantDuties.length});
173
+
174
+ const dutiesAtEpoch = this.dutiesByIndexByEpoch.get(epoch);
175
+ const priorDependentRoot = dutiesAtEpoch?.dependentRoot;
176
+ const dependentRootChanged = priorDependentRoot !== undefined && priorDependentRoot !== dependentRoot;
177
+
178
+ if (!priorDependentRoot || dependentRootChanged) {
179
+ const dutiesByIndex = new Map<ValidatorIndex, InclusionListDuty>();
180
+ for (const duty of relevantDuties) {
181
+ dutiesByIndex.set(duty.validatorIndex, duty);
182
+ }
183
+ this.dutiesByIndexByEpoch.set(epoch, {dependentRoot, dutiesByIndex});
184
+
185
+ if (priorDependentRoot && dependentRootChanged) {
186
+ this.logger.warn("Inclusion list duties re-org. This may happen from time to time", {
187
+ priorDependentRoot: priorDependentRoot,
188
+ dependentRoot: dependentRoot,
189
+ epoch,
190
+ });
191
+ }
192
+ } else {
193
+ const existingDuties = dutiesAtEpoch.dutiesByIndex;
194
+ const existingDutiesCount = existingDuties.size;
195
+ for (const duty of relevantDuties) {
196
+ if (!existingDuties.has(duty.validatorIndex)) {
197
+ existingDuties.set(duty.validatorIndex, duty);
198
+ }
199
+ }
200
+
201
+ if (existingDuties.size > existingDutiesCount) {
202
+ this.logger.debug("Discovered new inclusion list duties", {
203
+ epoch,
204
+ dependentRoot,
205
+ count: existingDuties.size - existingDutiesCount,
206
+ });
207
+ }
208
+ }
209
+ }
210
+
211
+ private async handleInclusionListDutiesReorg(
212
+ dutyEpoch: Epoch,
213
+ slot: Slot,
214
+ oldDependentRoot: RootHex,
215
+ newDependentRoot: RootHex
216
+ ): Promise<void> {
217
+ const logContext = {dutyEpoch, slot, oldDependentRoot, newDependentRoot};
218
+ this.logger.debug("Redownload inclusion list duties", logContext);
219
+
220
+ await this.pollInclusionListCommitteeForEpoch(dutyEpoch, this.validatorStore.getAllLocalIndices())
221
+ .then(() => {
222
+ this.pendingDependentRootByEpoch.delete(dutyEpoch);
223
+ })
224
+ .catch((e: Error) => {
225
+ this.logger.error("Failed to redownload inclusion list duties when reorg happens", logContext, e);
226
+ });
227
+ }
228
+
229
+ /**
230
+ * inclusion list duties may be reorged due to 2 scenarios:
231
+ * 1. node is syncing (for nextEpoch duties)
232
+ * 2. node is reorged
233
+ * previousDutyDependentRoot = get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)
234
+ * => dependent root of current epoch
235
+ * currentDutyDependentRoot = get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch) - 1)
236
+ * => dependent root of next epoch
237
+ */
238
+ private onNewHead = async ({
239
+ slot,
240
+ head,
241
+ previousDutyDependentRoot,
242
+ currentDutyDependentRoot,
243
+ }: HeadEventData): Promise<void> => {
244
+ const currentEpoch = computeEpochAtSlot(slot);
245
+ const nextEpoch = currentEpoch + 1;
246
+ const nextTwoEpoch = currentEpoch + 2;
247
+ const nextTwoEpochDependentRoot = this.dutiesByIndexByEpoch.get(currentEpoch + 2)?.dependentRoot;
248
+
249
+ // this may happen ONLY when node is syncing
250
+ // it's safe to get inclusion list duties at epoch n + 1 thanks to nextEpochShuffling cache
251
+ // but it's an issue to request inclusion list duties for epoch n + 2 as dependent root keeps changing while node is syncing
252
+ // see https://github.com/ChainSafe/lodestar/issues/3211
253
+ if (nextTwoEpochDependentRoot && head !== nextTwoEpochDependentRoot) {
254
+ // last slot of epoch, we're sure it's the correct dependent root
255
+ if ((slot + 1) % SLOTS_PER_EPOCH === 0) {
256
+ this.logger.info("Next 2 epoch inclusion list duties reorg", {slot, dutyEpoch: nextTwoEpoch, head});
257
+ await this.handleInclusionListDutiesReorg(nextTwoEpoch, slot, nextTwoEpochDependentRoot, head);
258
+ } else {
259
+ this.logger.debug("Potential next 2 epoch inclusion list duties reorg", {slot, dutyEpoch: nextTwoEpoch, head});
260
+ // node may send adjacent onHead events while it's syncing
261
+ // wait for getting close to next epoch to make sure the dependRoot
262
+ this.pendingDependentRootByEpoch.set(nextTwoEpoch, head);
263
+ }
264
+ }
265
+
266
+ // dependent root for next epoch changed
267
+ const nextEpochDependentRoot = this.dutiesByIndexByEpoch.get(nextEpoch)?.dependentRoot;
268
+ if (nextEpochDependentRoot && currentDutyDependentRoot !== nextEpochDependentRoot) {
269
+ this.logger.warn("Potential next epoch inclusion list duties reorg", {
270
+ slot,
271
+ dutyEpoch: nextEpoch,
272
+ priorDependentRoot: nextEpochDependentRoot,
273
+ newDependentRoot: currentDutyDependentRoot,
274
+ });
275
+ await this.handleInclusionListDutiesReorg(nextEpoch, slot, nextEpochDependentRoot, currentDutyDependentRoot);
276
+ }
277
+
278
+ // dependent root for current epoch changed
279
+ const currentEpochDependentRoot = this.dutiesByIndexByEpoch.get(currentEpoch)?.dependentRoot;
280
+ if (currentEpochDependentRoot && currentEpochDependentRoot !== previousDutyDependentRoot) {
281
+ this.logger.warn("Potential current epoch inclusion list duties reorg", {
282
+ slot,
283
+ dutyEpoch: currentEpoch,
284
+ priorDependentRoot: currentEpochDependentRoot,
285
+ newDependentRoot: previousDutyDependentRoot,
286
+ });
287
+ await this.handleInclusionListDutiesReorg(
288
+ currentEpoch,
289
+ slot,
290
+ currentEpochDependentRoot,
291
+ previousDutyDependentRoot
292
+ );
293
+ }
294
+ };
295
+
296
+ /** Run once per epoch to prune duties map */
297
+ private pruneOldDuties(currentEpoch: Epoch): void {
298
+ for (const byEpochMap of [this.dutiesByIndexByEpoch, this.pendingDependentRootByEpoch]) {
299
+ for (const epoch of byEpochMap.keys()) {
300
+ if (epoch + HISTORICAL_DUTIES_EPOCHS < currentEpoch) {
301
+ byEpochMap.delete(epoch);
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
@@ -11,6 +11,7 @@ import {
11
11
  DOMAIN_BEACON_PROPOSER,
12
12
  DOMAIN_BUILDER_REQUEST_AUTH,
13
13
  DOMAIN_CONTRIBUTION_AND_PROOF,
14
+ DOMAIN_INCLUSION_LIST_COMMITTEE,
14
15
  DOMAIN_PROPOSER_PREFERENCES,
15
16
  DOMAIN_PTC_ATTESTER,
16
17
  DOMAIN_RANDAO,
@@ -46,6 +47,7 @@ import {
46
47
  altair,
47
48
  bellatrix,
48
49
  gloas,
50
+ heze,
49
51
  phase0,
50
52
  ssz,
51
53
  } from "@lodestar/types";
@@ -954,6 +956,38 @@ export class ValidatorStore {
954
956
  };
955
957
  }
956
958
 
959
+ async signInclusionList(
960
+ duty: routes.validator.InclusionListDuty,
961
+ inclusionList: heze.InclusionList
962
+ ): Promise<heze.SignedInclusionList> {
963
+ if (duty.slot !== inclusionList.slot) {
964
+ throw Error(
965
+ `Inconsistent duties during signing: duty.slot ${duty.slot} != inclusionList.slot ${inclusionList.slot}`
966
+ );
967
+ }
968
+ if (duty.validatorIndex !== inclusionList.validatorIndex) {
969
+ throw Error(
970
+ `Inconsistent duties during signing: duty.validatorIndex ${duty.validatorIndex} != inclusionList.validatorIndex ${inclusionList.validatorIndex}`
971
+ );
972
+ }
973
+
974
+ this.assertDoppelgangerSafe(duty.pubkey);
975
+
976
+ const signingSlot = inclusionList.slot;
977
+ const domain = this.config.getDomain(signingSlot, DOMAIN_INCLUSION_LIST_COMMITTEE);
978
+ const signingRoot = computeSigningRoot(ssz.heze.InclusionList, inclusionList, domain);
979
+
980
+ const signableMessage: SignableMessage = {
981
+ type: SignableMessageType.INCLUSION_LIST,
982
+ data: inclusionList,
983
+ };
984
+
985
+ return {
986
+ message: inclusionList,
987
+ signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
988
+ };
989
+ }
990
+
957
991
  async signAttestationSelectionProof(pubkey: BLSPubkeyMaybeHex, slot: Slot): Promise<BLSSignature> {
958
992
  const signingSlot = slot;
959
993
  const domain = this.config.getDomain(slot, DOMAIN_SELECTION_PROOF);
@@ -1214,6 +1248,11 @@ export class ValidatorStore {
1214
1248
  }
1215
1249
  }
1216
1250
 
1251
+ /**
1252
+ * Required before signing a message that is slashable or that the beacon node counts as validator
1253
+ * liveness (see `validatorSeenAtEpoch`), otherwise doppelganger detection would flag our own messages.
1254
+ * Skip it for anything else.
1255
+ */
1217
1256
  private assertDoppelgangerSafe(pubKey: PubkeyHex | BLSPubkey): void {
1218
1257
  const pubkeyHex = typeof pubKey === "string" ? pubKey : toPubkeyHex(pubKey);
1219
1258
  if (!this.isDoppelgangerSafe(pubkeyHex)) {
@@ -12,6 +12,7 @@ import {
12
12
  Slot,
13
13
  altair,
14
14
  gloas,
15
+ heze,
15
16
  phase0,
16
17
  ssz,
17
18
  sszTypesFor,
@@ -37,6 +38,7 @@ export enum SignableMessageType {
37
38
  PAYLOAD_ATTESTATION_MESSAGE = "PAYLOAD_ATTESTATION_MESSAGE",
38
39
  PROPOSER_PREFERENCES = "PROPOSER_PREFERENCES",
39
40
  BUILDER_REQUEST_AUTH = "BUILDER_REQUEST_AUTH",
41
+ INCLUSION_LIST = "INCLUSION_LIST",
40
42
  }
41
43
 
42
44
  const AggregationSlotType = new ContainerType({
@@ -89,7 +91,8 @@ export type SignableMessage =
89
91
  | {type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE; data: gloas.ExecutionPayloadEnvelope}
90
92
  | {type: SignableMessageType.PAYLOAD_ATTESTATION_MESSAGE; data: gloas.PayloadAttestationData}
91
93
  | {type: SignableMessageType.PROPOSER_PREFERENCES; data: gloas.ProposerPreferences}
92
- | {type: SignableMessageType.BUILDER_REQUEST_AUTH; data: gloas.BuilderRequestAuth};
94
+ | {type: SignableMessageType.BUILDER_REQUEST_AUTH; data: gloas.BuilderRequestAuth}
95
+ | {type: SignableMessageType.INCLUSION_LIST; data: heze.InclusionList};
93
96
 
94
97
  const requiresForkInfo: Record<SignableMessageType, boolean> = {
95
98
  [SignableMessageType.AGGREGATION_SLOT]: true,
@@ -108,6 +111,7 @@ const requiresForkInfo: Record<SignableMessageType, boolean> = {
108
111
  [SignableMessageType.PAYLOAD_ATTESTATION_MESSAGE]: true,
109
112
  [SignableMessageType.PROPOSER_PREFERENCES]: true,
110
113
  [SignableMessageType.BUILDER_REQUEST_AUTH]: false,
114
+ [SignableMessageType.INCLUSION_LIST]: true,
111
115
  };
112
116
 
113
117
  type Web3SignerSerializedRequest = {
@@ -319,6 +323,9 @@ function serializerSignableMessagePayload(config: BeaconConfig, payload: Signabl
319
323
  },
320
324
  };
321
325
  }
326
+
327
+ case SignableMessageType.INCLUSION_LIST:
328
+ return {inclusion_list: ssz.heze.InclusionList.toJson(payload.data)};
322
329
  }
323
330
  }
324
331
 
package/src/validator.ts CHANGED
@@ -27,6 +27,7 @@ import {ChainHeaderTracker} from "./services/chainHeaderTracker.js";
27
27
  import {DoppelgangerService} from "./services/doppelgangerService.js";
28
28
  import {ValidatorEventEmitter} from "./services/emitter.js";
29
29
  import {ExternalSignerOptions, pollExternalSignerPubkeys} from "./services/externalSignerSync.js";
30
+ import {InclusionListService} from "./services/inclusionList.js";
30
31
  import {IndicesService} from "./services/indices.js";
31
32
  import {pollBuilderValidatorRegistration, pollPrepareBeaconProposer} from "./services/prepareBeaconProposer.js";
32
33
  import {ProposalPreferencesService} from "./services/proposalPreferences.js";
@@ -53,6 +54,7 @@ export type ValidatorModules = {
53
54
  blockProposingService: BlockProposingService;
54
55
  attestationService: AttestationService;
55
56
  ptcService: PtcService;
57
+ inclusionListService: InclusionListService;
56
58
  syncCommitteeService: SyncCommitteeService;
57
59
  config: BeaconConfig;
58
60
  api: ApiClient;
@@ -109,6 +111,7 @@ export class Validator {
109
111
  private readonly blockProposingService: BlockProposingService;
110
112
  private readonly attestationService: AttestationService;
111
113
  private readonly ptcService: PtcService;
114
+ private readonly inclusionListService: InclusionListService;
112
115
  private readonly syncCommitteeService: SyncCommitteeService;
113
116
  private readonly config: BeaconConfig;
114
117
  private readonly api: ApiClient;
@@ -128,6 +131,7 @@ export class Validator {
128
131
  blockProposingService,
129
132
  attestationService,
130
133
  ptcService,
134
+ inclusionListService,
131
135
  syncCommitteeService,
132
136
  config,
133
137
  api,
@@ -145,6 +149,7 @@ export class Validator {
145
149
  this.blockProposingService = blockProposingService;
146
150
  this.attestationService = attestationService;
147
151
  this.ptcService = ptcService;
152
+ this.inclusionListService = inclusionListService;
148
153
  this.syncCommitteeService = syncCommitteeService;
149
154
  this.config = config;
150
155
  this.api = api;
@@ -312,6 +317,17 @@ export class Validator {
312
317
  metrics
313
318
  );
314
319
 
320
+ const inclusionListService = new InclusionListService(
321
+ config,
322
+ loggerVc,
323
+ api,
324
+ clock,
325
+ validatorStore,
326
+ chainHeaderTracker,
327
+ syncingStatusTracker,
328
+ emitter
329
+ );
330
+
315
331
  const syncCommitteeService = new SyncCommitteeService(
316
332
  config,
317
333
  loggerVc,
@@ -347,6 +363,7 @@ export class Validator {
347
363
  blockProposingService,
348
364
  attestationService,
349
365
  ptcService,
366
+ inclusionListService,
350
367
  syncCommitteeService,
351
368
  config,
352
369
  api,
@@ -433,6 +450,7 @@ export class Validator {
433
450
  this.blockProposingService.removeDutiesForKey(pubkey);
434
451
  this.attestationService.removeDutiesForKey(pubkey);
435
452
  this.ptcService.removeDutiesForKey(pubkey);
453
+ this.inclusionListService.removeDutiesForKey(pubkey);
436
454
  this.syncCommitteeService.removeDutiesForKey(pubkey);
437
455
  }
438
456