@lodestar/validator 1.35.0-dev.98d359db41 → 1.35.0-dev.a70bac5bd3

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 (169) hide show
  1. package/lib/index.d.ts +7 -7
  2. package/lib/index.js +5 -5
  3. package/lib/index.js.map +1 -1
  4. package/lib/repositories/metaDataRepository.js +3 -4
  5. package/lib/repositories/metaDataRepository.js.map +1 -1
  6. package/lib/services/attestation.js +44 -53
  7. package/lib/services/attestation.js.map +1 -1
  8. package/lib/services/attestationDuties.js +98 -104
  9. package/lib/services/attestationDuties.js.map +1 -1
  10. package/lib/services/block.js +56 -64
  11. package/lib/services/block.js.map +1 -1
  12. package/lib/services/blockDuties.js +23 -31
  13. package/lib/services/blockDuties.js.map +1 -1
  14. package/lib/services/chainHeaderTracker.js +27 -30
  15. package/lib/services/chainHeaderTracker.js.map +1 -1
  16. package/lib/services/doppelgangerService.js +45 -52
  17. package/lib/services/doppelgangerService.js.map +1 -1
  18. package/lib/services/emitter.d.ts +1 -1
  19. package/lib/services/externalSignerSync.js.map +1 -1
  20. package/lib/services/indices.js +5 -8
  21. package/lib/services/indices.js.map +1 -1
  22. package/lib/services/prepareBeaconProposer.js.map +1 -1
  23. package/lib/services/syncCommittee.js +49 -60
  24. package/lib/services/syncCommittee.js.map +1 -1
  25. package/lib/services/syncCommitteeDuties.js +23 -28
  26. package/lib/services/syncCommitteeDuties.js.map +1 -1
  27. package/lib/services/syncingStatusTracker.js +27 -32
  28. package/lib/services/syncingStatusTracker.js.map +1 -1
  29. package/lib/services/validatorStore.js +3 -9
  30. package/lib/services/validatorStore.js.map +1 -1
  31. package/lib/slashingProtection/attestation/attestationByTargetRepository.js +3 -7
  32. package/lib/slashingProtection/attestation/attestationByTargetRepository.js.map +1 -1
  33. package/lib/slashingProtection/attestation/attestationLowerBoundRepository.js +3 -5
  34. package/lib/slashingProtection/attestation/attestationLowerBoundRepository.js.map +1 -1
  35. package/lib/slashingProtection/attestation/index.js +0 -3
  36. package/lib/slashingProtection/attestation/index.js.map +1 -1
  37. package/lib/slashingProtection/block/blockBySlotRepository.js +3 -7
  38. package/lib/slashingProtection/block/blockBySlotRepository.js.map +1 -1
  39. package/lib/slashingProtection/block/index.js +0 -1
  40. package/lib/slashingProtection/block/index.js.map +1 -1
  41. package/lib/slashingProtection/index.d.ts +1 -1
  42. package/lib/slashingProtection/index.js +0 -3
  43. package/lib/slashingProtection/index.js.map +1 -1
  44. package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.js +0 -8
  45. package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.js.map +1 -1
  46. package/lib/slashingProtection/minMaxSurround/minMaxSurround.js +0 -2
  47. package/lib/slashingProtection/minMaxSurround/minMaxSurround.js.map +1 -1
  48. package/lib/slashingProtection/utils.d.ts +1 -1
  49. package/lib/util/clock.js +1 -5
  50. package/lib/util/clock.js.map +1 -1
  51. package/lib/util/params.js +0 -9
  52. package/lib/util/params.js.map +1 -1
  53. package/lib/validator.js +0 -15
  54. package/lib/validator.js.map +1 -1
  55. package/package.json +16 -19
  56. package/lib/buckets.d.ts.map +0 -1
  57. package/lib/defaults.d.ts.map +0 -1
  58. package/lib/genesis.d.ts.map +0 -1
  59. package/lib/index.d.ts.map +0 -1
  60. package/lib/metrics.d.ts.map +0 -1
  61. package/lib/repositories/index.d.ts.map +0 -1
  62. package/lib/repositories/metaDataRepository.d.ts.map +0 -1
  63. package/lib/services/attestation.d.ts.map +0 -1
  64. package/lib/services/attestationDuties.d.ts.map +0 -1
  65. package/lib/services/block.d.ts.map +0 -1
  66. package/lib/services/blockDuties.d.ts.map +0 -1
  67. package/lib/services/chainHeaderTracker.d.ts.map +0 -1
  68. package/lib/services/doppelgangerService.d.ts.map +0 -1
  69. package/lib/services/emitter.d.ts.map +0 -1
  70. package/lib/services/externalSignerSync.d.ts.map +0 -1
  71. package/lib/services/indices.d.ts.map +0 -1
  72. package/lib/services/prepareBeaconProposer.d.ts.map +0 -1
  73. package/lib/services/syncCommittee.d.ts.map +0 -1
  74. package/lib/services/syncCommitteeDuties.d.ts.map +0 -1
  75. package/lib/services/syncingStatusTracker.d.ts.map +0 -1
  76. package/lib/services/utils.d.ts.map +0 -1
  77. package/lib/services/validatorStore.d.ts.map +0 -1
  78. package/lib/slashingProtection/attestation/attestationByTargetRepository.d.ts.map +0 -1
  79. package/lib/slashingProtection/attestation/attestationLowerBoundRepository.d.ts.map +0 -1
  80. package/lib/slashingProtection/attestation/errors.d.ts.map +0 -1
  81. package/lib/slashingProtection/attestation/index.d.ts.map +0 -1
  82. package/lib/slashingProtection/block/blockBySlotRepository.d.ts.map +0 -1
  83. package/lib/slashingProtection/block/errors.d.ts.map +0 -1
  84. package/lib/slashingProtection/block/index.d.ts.map +0 -1
  85. package/lib/slashingProtection/index.d.ts.map +0 -1
  86. package/lib/slashingProtection/interchange/errors.d.ts.map +0 -1
  87. package/lib/slashingProtection/interchange/formats/completeV4.d.ts.map +0 -1
  88. package/lib/slashingProtection/interchange/formats/index.d.ts.map +0 -1
  89. package/lib/slashingProtection/interchange/formats/v5.d.ts.map +0 -1
  90. package/lib/slashingProtection/interchange/index.d.ts.map +0 -1
  91. package/lib/slashingProtection/interchange/parseInterchange.d.ts.map +0 -1
  92. package/lib/slashingProtection/interchange/serializeInterchange.d.ts.map +0 -1
  93. package/lib/slashingProtection/interchange/types.d.ts.map +0 -1
  94. package/lib/slashingProtection/interface.d.ts.map +0 -1
  95. package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.d.ts.map +0 -1
  96. package/lib/slashingProtection/minMaxSurround/errors.d.ts.map +0 -1
  97. package/lib/slashingProtection/minMaxSurround/index.d.ts.map +0 -1
  98. package/lib/slashingProtection/minMaxSurround/interface.d.ts.map +0 -1
  99. package/lib/slashingProtection/minMaxSurround/minMaxSurround.d.ts.map +0 -1
  100. package/lib/slashingProtection/types.d.ts.map +0 -1
  101. package/lib/slashingProtection/utils.d.ts.map +0 -1
  102. package/lib/types.d.ts.map +0 -1
  103. package/lib/util/batch.d.ts.map +0 -1
  104. package/lib/util/clock.d.ts.map +0 -1
  105. package/lib/util/difference.d.ts.map +0 -1
  106. package/lib/util/externalSignerClient.d.ts.map +0 -1
  107. package/lib/util/format.d.ts.map +0 -1
  108. package/lib/util/index.d.ts.map +0 -1
  109. package/lib/util/logger.d.ts.map +0 -1
  110. package/lib/util/params.d.ts.map +0 -1
  111. package/lib/util/url.d.ts.map +0 -1
  112. package/lib/validator.d.ts.map +0 -1
  113. package/src/buckets.ts +0 -30
  114. package/src/defaults.ts +0 -8
  115. package/src/genesis.ts +0 -19
  116. package/src/index.ts +0 -22
  117. package/src/metrics.ts +0 -417
  118. package/src/repositories/index.ts +0 -1
  119. package/src/repositories/metaDataRepository.ts +0 -42
  120. package/src/services/attestation.ts +0 -349
  121. package/src/services/attestationDuties.ts +0 -405
  122. package/src/services/block.ts +0 -261
  123. package/src/services/blockDuties.ts +0 -215
  124. package/src/services/chainHeaderTracker.ts +0 -89
  125. package/src/services/doppelgangerService.ts +0 -286
  126. package/src/services/emitter.ts +0 -43
  127. package/src/services/externalSignerSync.ts +0 -81
  128. package/src/services/indices.ts +0 -165
  129. package/src/services/prepareBeaconProposer.ts +0 -119
  130. package/src/services/syncCommittee.ts +0 -317
  131. package/src/services/syncCommitteeDuties.ts +0 -337
  132. package/src/services/syncingStatusTracker.ts +0 -74
  133. package/src/services/utils.ts +0 -58
  134. package/src/services/validatorStore.ts +0 -830
  135. package/src/slashingProtection/attestation/attestationByTargetRepository.ts +0 -77
  136. package/src/slashingProtection/attestation/attestationLowerBoundRepository.ts +0 -44
  137. package/src/slashingProtection/attestation/errors.ts +0 -66
  138. package/src/slashingProtection/attestation/index.ts +0 -171
  139. package/src/slashingProtection/block/blockBySlotRepository.ts +0 -78
  140. package/src/slashingProtection/block/errors.ts +0 -28
  141. package/src/slashingProtection/block/index.ts +0 -94
  142. package/src/slashingProtection/index.ts +0 -95
  143. package/src/slashingProtection/interchange/errors.ts +0 -15
  144. package/src/slashingProtection/interchange/formats/completeV4.ts +0 -125
  145. package/src/slashingProtection/interchange/formats/index.ts +0 -7
  146. package/src/slashingProtection/interchange/formats/v5.ts +0 -120
  147. package/src/slashingProtection/interchange/index.ts +0 -5
  148. package/src/slashingProtection/interchange/parseInterchange.ts +0 -55
  149. package/src/slashingProtection/interchange/serializeInterchange.ts +0 -35
  150. package/src/slashingProtection/interchange/types.ts +0 -18
  151. package/src/slashingProtection/interface.ts +0 -28
  152. package/src/slashingProtection/minMaxSurround/distanceStoreRepository.ts +0 -57
  153. package/src/slashingProtection/minMaxSurround/errors.ts +0 -27
  154. package/src/slashingProtection/minMaxSurround/index.ts +0 -4
  155. package/src/slashingProtection/minMaxSurround/interface.ts +0 -23
  156. package/src/slashingProtection/minMaxSurround/minMaxSurround.ts +0 -104
  157. package/src/slashingProtection/types.ts +0 -12
  158. package/src/slashingProtection/utils.ts +0 -42
  159. package/src/types.ts +0 -31
  160. package/src/util/batch.ts +0 -15
  161. package/src/util/clock.ts +0 -164
  162. package/src/util/difference.ts +0 -10
  163. package/src/util/externalSignerClient.ts +0 -277
  164. package/src/util/format.ts +0 -3
  165. package/src/util/index.ts +0 -6
  166. package/src/util/logger.ts +0 -51
  167. package/src/util/params.ts +0 -313
  168. package/src/util/url.ts +0 -16
  169. package/src/validator.ts +0 -418
@@ -1,215 +0,0 @@
1
- import {ApiClient, routes} from "@lodestar/api";
2
- import {ChainConfig} from "@lodestar/config";
3
- import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
- import {BLSPubkey, Epoch, RootHex, Slot} from "@lodestar/types";
5
- import {sleep, toPubkeyHex} from "@lodestar/utils";
6
- import {Metrics} from "../metrics.js";
7
- import {PubkeyHex} from "../types.js";
8
- import {IClock, LoggerVc, differenceHex} from "../util/index.js";
9
- import {ValidatorStore} from "./validatorStore.js";
10
-
11
- /** This polls block duties 1s before the next epoch */
12
- // TODO: change to 6 to do it 2s before the next epoch
13
- // once we have some improvement on epoch transition time
14
- // see https://github.com/ChainSafe/lodestar/issues/5792#issuecomment-1647457442
15
- const BLOCK_DUTIES_LOOKAHEAD_FACTOR = 12;
16
- /** Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch */
17
- const HISTORICAL_DUTIES_EPOCHS = 2;
18
- // Re-declaring to not have to depend on `lodestar-params` just for this 0
19
- const GENESIS_EPOCH = 0;
20
- export const GENESIS_SLOT = 0;
21
-
22
- type BlockDutyAtEpoch = {dependentRoot: RootHex; data: routes.validator.ProposerDuty[]};
23
- type NotifyBlockProductionFn = (slot: Slot, proposers: BLSPubkey[]) => void;
24
-
25
- export class BlockDutiesService {
26
- /** Notify the block service if it should produce a block. */
27
- private readonly notifyBlockProductionFn: NotifyBlockProductionFn;
28
- /** Maps an epoch to all *local* proposers in this epoch. Notably, this does not contain
29
- proposals for any validators which are not registered locally. */
30
- private readonly proposers = new Map<Epoch, BlockDutyAtEpoch>();
31
-
32
- constructor(
33
- private readonly config: ChainConfig,
34
- private readonly logger: LoggerVc,
35
- private readonly api: ApiClient,
36
- private readonly clock: IClock,
37
- private readonly validatorStore: ValidatorStore,
38
- private readonly metrics: Metrics | null,
39
- notifyBlockProductionFn: NotifyBlockProductionFn
40
- ) {
41
- this.notifyBlockProductionFn = notifyBlockProductionFn;
42
-
43
- // TODO: Instead of polling every CLOCK_SLOT, poll every CLOCK_EPOCH and track re-org events
44
- // only then re-fetch the block duties. Make sure most clients (including Lodestar)
45
- // properly emit the re-org event
46
- clock.runEverySlot(this.runBlockDutiesTask);
47
-
48
- if (metrics) {
49
- metrics.proposerDutiesEpochCount.addCollect(() => {
50
- metrics.proposerDutiesEpochCount.set(this.proposers.size);
51
- });
52
- }
53
- }
54
-
55
- /**
56
- * Returns the pubkeys of the validators which are assigned to propose in the given slot.
57
- *
58
- * It is possible that multiple validators have an identical proposal slot, however that is
59
- * likely the result of heavy forking (lol) or inconsistent beacon node connections.
60
- */
61
- getblockProposersAtSlot(slot: Slot): BLSPubkey[] {
62
- const epoch = computeEpochAtSlot(slot);
63
- const publicKeys = new Map<string, BLSPubkey>(); // pseudo-HashSet of Buffers
64
-
65
- const dutyAtEpoch = this.proposers.get(epoch);
66
- if (dutyAtEpoch) {
67
- for (const proposer of dutyAtEpoch.data) {
68
- if (proposer.slot === slot) {
69
- publicKeys.set(toPubkeyHex(proposer.pubkey), proposer.pubkey);
70
- }
71
- }
72
- }
73
-
74
- return Array.from(publicKeys.values());
75
- }
76
-
77
- removeDutiesForKey(pubkey: PubkeyHex): void {
78
- for (const blockDutyAtEpoch of this.proposers.values()) {
79
- blockDutyAtEpoch.data = blockDutyAtEpoch.data.filter((proposer) => {
80
- return toPubkeyHex(proposer.pubkey) !== pubkey;
81
- });
82
- }
83
- }
84
-
85
- private runBlockDutiesTask = async (slot: Slot, signal: AbortSignal): Promise<void> => {
86
- try {
87
- if (slot < 0) {
88
- // Before genesis, fetch the genesis duties but don't notify block production
89
- // Only fetch duties once since there is not possible to re-org. TODO: Review
90
- if (!this.proposers.has(GENESIS_EPOCH)) {
91
- await this.pollBeaconProposers(GENESIS_EPOCH);
92
- }
93
- } else {
94
- await this.pollBeaconProposersAndNotify(slot, signal);
95
- }
96
- } catch (e) {
97
- this.logger.error("Error on pollBeaconProposers", {}, e as Error);
98
- } finally {
99
- this.pruneOldDuties(computeEpochAtSlot(slot));
100
- }
101
- };
102
-
103
- /**
104
- * Download the proposer duties for the current epoch and store them in `this.proposers`.
105
- * If there are any proposer for this slot, send out a notification to the block proposers.
106
- *
107
- * ## Note
108
- *
109
- * This function will potentially send *two* notifications to the `BlockService`; it will send a
110
- * notification initially, then it will download the latest duties and send a *second* notification
111
- * if those duties have changed. This behaviour simultaneously achieves the following:
112
- *
113
- * 1. Block production can happen immediately and does not have to wait for the proposer duties to
114
- * download.
115
- * 2. We won't miss a block if the duties for the current slot happen to change with this poll.
116
- *
117
- * This sounds great, but is it safe? Firstly, the additional notification will only contain block
118
- * producers that were not included in the first notification. This should be safety enough.
119
- * However, we also have the slashing protection as a second line of defense. These two factors
120
- * provide an acceptable level of safety.
121
- *
122
- * It's important to note that since there is a 0-epoch look-ahead (i.e., no look-ahead) for block
123
- * proposers then it's very likely that a proposal for the first slot of the epoch will need go
124
- * through the slow path every time. I.e., the proposal will only happen after we've been able to
125
- * download and process the duties from the BN. This means it is very important to ensure this
126
- * function is as fast as possible.
127
- * - Starting from Jul 2023, we poll proposers 1s before the next epoch thanks to PrepareNextSlotScheduler
128
- * usually finishes in 3s.
129
- */
130
- private async pollBeaconProposersAndNotify(currentSlot: Slot, signal: AbortSignal): Promise<void> {
131
- const nextEpoch = computeEpochAtSlot(currentSlot) + 1;
132
- const isLastSlotEpoch = computeStartSlotAtEpoch(nextEpoch) === currentSlot + 1;
133
- if (isLastSlotEpoch) {
134
- // no need to await for other steps, just poll proposers for next epoch
135
- this.pollBeaconProposersNextEpoch(currentSlot, nextEpoch, signal).catch((e) => {
136
- this.logger.error("Error on pollBeaconProposersNextEpoch", {}, e);
137
- });
138
- }
139
-
140
- // Notify the block proposal service for any proposals that we have in our cache.
141
- const initialBlockProposers = this.getblockProposersAtSlot(currentSlot);
142
- if (initialBlockProposers.length > 0) {
143
- this.notifyBlockProductionFn(currentSlot, initialBlockProposers);
144
- }
145
-
146
- // Poll proposers again for the same slot
147
- await this.pollBeaconProposers(computeEpochAtSlot(currentSlot));
148
-
149
- // Compute the block proposers for this slot again, now that we've received an update from the BN.
150
- //
151
- // Then, compute the difference between these two sets to obtain a set of block proposers
152
- // which were not included in the initial notification to the `BlockService`.
153
- const newBlockProducers = this.getblockProposersAtSlot(currentSlot);
154
- const additionalBlockProducers = differenceHex(initialBlockProposers, newBlockProducers);
155
-
156
- // If there are any new proposers for this slot, send a notification so they produce a block.
157
- //
158
- // See the function-level documentation for more reasoning about this behaviour.
159
- if (additionalBlockProducers.length > 0) {
160
- this.notifyBlockProductionFn(currentSlot, additionalBlockProducers);
161
- this.logger.debug("Detected new block proposer", {currentSlot});
162
- this.metrics?.newProposalDutiesDetected.inc();
163
- }
164
- }
165
-
166
- /**
167
- * This is to avoid some delay on the first slot of the epoch when validators have proposal duties.
168
- * See https://github.com/ChainSafe/lodestar/issues/5792
169
- */
170
- private async pollBeaconProposersNextEpoch(currentSlot: Slot, nextEpoch: Epoch, signal: AbortSignal): Promise<void> {
171
- const nextSlot = currentSlot + 1;
172
- const lookAheadMs = (this.config.SECONDS_PER_SLOT * 1000) / BLOCK_DUTIES_LOOKAHEAD_FACTOR;
173
- await sleep(this.clock.msToSlot(nextSlot) - lookAheadMs, signal);
174
- this.logger.debug("Polling proposers for next epoch", {nextEpoch, nextSlot});
175
- // Poll proposers for the next epoch
176
- await this.pollBeaconProposers(nextEpoch);
177
- }
178
-
179
- private async pollBeaconProposers(epoch: Epoch): Promise<void> {
180
- // Only download duties and push out additional block production events if we have some validators.
181
- if (!this.validatorStore.hasSomeValidators()) {
182
- return;
183
- }
184
-
185
- const res = await this.api.validator.getProposerDuties({epoch});
186
- const proposerDuties = res.value();
187
- const {dependentRoot} = res.meta();
188
- const relevantDuties = proposerDuties.filter((duty) => {
189
- const pubkeyHex = toPubkeyHex(duty.pubkey);
190
- return this.validatorStore.hasVotingPubkey(pubkeyHex) && this.validatorStore.isDoppelgangerSafe(pubkeyHex);
191
- });
192
-
193
- this.logger.debug("Downloaded proposer duties", {epoch, dependentRoot, count: relevantDuties.length});
194
-
195
- const prior = this.proposers.get(epoch);
196
- this.proposers.set(epoch, {dependentRoot, data: relevantDuties});
197
-
198
- if (prior && prior.dependentRoot !== dependentRoot) {
199
- this.metrics?.proposerDutiesReorg.inc();
200
- this.logger.warn("Proposer duties re-org. This may happen from time to time", {
201
- priorDependentRoot: prior.dependentRoot,
202
- dependentRoot,
203
- });
204
- }
205
- }
206
-
207
- /** Run once per epoch to prune `this.proposers` map */
208
- private pruneOldDuties(currentEpoch: Epoch): void {
209
- for (const epoch of this.proposers.keys()) {
210
- if (epoch + HISTORICAL_DUTIES_EPOCHS < currentEpoch) {
211
- this.proposers.delete(epoch);
212
- }
213
- }
214
- }
215
- }
@@ -1,89 +0,0 @@
1
- import {ApiClient, routes} from "@lodestar/api";
2
- import {GENESIS_SLOT} from "@lodestar/params";
3
- import {Root, RootHex, Slot} from "@lodestar/types";
4
- import {Logger, fromHex} from "@lodestar/utils";
5
- import {ValidatorEvent, ValidatorEventEmitter} from "./emitter.js";
6
-
7
- const {EventType} = routes.events;
8
-
9
- export type HeadEventData = {
10
- slot: Slot;
11
- head: RootHex;
12
- previousDutyDependentRoot: RootHex;
13
- currentDutyDependentRoot: RootHex;
14
- };
15
-
16
- type RunEveryFn = (event: HeadEventData) => Promise<void>;
17
-
18
- /**
19
- * Track the head slot/root using the event stream api "head".
20
- */
21
- export class ChainHeaderTracker {
22
- private headBlockSlot: Slot = GENESIS_SLOT;
23
- private headBlockRoot: Root | null = null;
24
- private readonly fns: RunEveryFn[] = [];
25
-
26
- constructor(
27
- private readonly logger: Logger,
28
- private readonly api: ApiClient,
29
- private readonly emitter: ValidatorEventEmitter
30
- ) {}
31
-
32
- start(signal: AbortSignal): void {
33
- this.logger.verbose("Subscribing to head event");
34
- this.api.events
35
- .eventstream({
36
- topics: [EventType.head],
37
- signal,
38
- onEvent: this.onHeadUpdate,
39
- onError: (e) => {
40
- this.logger.error("Failed to receive head event", {}, e);
41
- },
42
- onClose: () => {
43
- this.logger.verbose("Closed stream for head event", {});
44
- },
45
- })
46
- .catch((e) => this.logger.error("Failed to subscribe to head event", {}, e));
47
- }
48
-
49
- getCurrentChainHead(slot: Slot): Root | null {
50
- if (slot >= this.headBlockSlot) {
51
- return this.headBlockRoot;
52
- }
53
- // We don't know head of an old block
54
- return null;
55
- }
56
-
57
- runOnNewHead(fn: RunEveryFn): void {
58
- this.fns.push(fn);
59
- }
60
-
61
- private onHeadUpdate = (event: routes.events.BeaconEvent): void => {
62
- if (event.type === EventType.head) {
63
- const {message} = event;
64
- const {slot, block, previousDutyDependentRoot, currentDutyDependentRoot} = message;
65
- this.headBlockSlot = slot;
66
- this.headBlockRoot = fromHex(block);
67
-
68
- const headEventData = {
69
- slot: this.headBlockSlot,
70
- head: block,
71
- previousDutyDependentRoot: previousDutyDependentRoot,
72
- currentDutyDependentRoot: currentDutyDependentRoot,
73
- };
74
-
75
- for (const fn of this.fns) {
76
- fn(headEventData).catch((e) => this.logger.error("Error calling head event handler", e));
77
- }
78
-
79
- this.emitter.emit(ValidatorEvent.chainHead, headEventData);
80
-
81
- this.logger.verbose("Found new chain head", {
82
- slot: slot,
83
- head: block,
84
- previousDuty: previousDutyDependentRoot,
85
- currentDuty: currentDutyDependentRoot,
86
- });
87
- }
88
- };
89
- }
@@ -1,286 +0,0 @@
1
- import {ApiClient, routes} from "@lodestar/api";
2
- import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
3
- import {Epoch, ValidatorIndex} from "@lodestar/types";
4
- import {Logger, fromHex, sleep, truncBytes} from "@lodestar/utils";
5
- import {Metrics} from "../metrics.js";
6
- import {ISlashingProtection} from "../slashingProtection/index.js";
7
- import {ProcessShutdownCallback, PubkeyHex} from "../types.js";
8
- import {IClock} from "../util/index.js";
9
- import {IndicesService} from "./indices.js";
10
-
11
- // The number of epochs that must be checked before we assume that there are
12
- // no other duplicate validators on the network
13
- const DEFAULT_REMAINING_DETECTION_EPOCHS = 1;
14
- const REMAINING_EPOCHS_IF_DOPPELGANGER = Infinity;
15
- const REMAINING_EPOCHS_IF_SKIPPED = 0;
16
-
17
- /** Liveness responses for a given epoch */
18
- type EpochLivenessData = {
19
- epoch: Epoch;
20
- responses: routes.validator.LivenessResponseData[];
21
- };
22
-
23
- export type DoppelgangerState = {
24
- nextEpochToCheck: Epoch;
25
- remainingEpochs: Epoch;
26
- };
27
-
28
- export enum DoppelgangerStatus {
29
- /** This pubkey is known to the doppelganger service and has been verified safe */
30
- VerifiedSafe = "VerifiedSafe",
31
- /** This pubkey is known to the doppelganger service but has not been verified safe */
32
- Unverified = "Unverified",
33
- /** This pubkey is unknown to the doppelganger service */
34
- Unknown = "Unknown",
35
- /** This pubkey has been detected to be active on the network */
36
- DoppelgangerDetected = "DoppelgangerDetected",
37
- }
38
-
39
- export class DoppelgangerService {
40
- private readonly doppelgangerStateByPubkey = new Map<PubkeyHex, DoppelgangerState>();
41
-
42
- constructor(
43
- private readonly logger: Logger,
44
- private readonly clock: IClock,
45
- private readonly api: ApiClient,
46
- private readonly indicesService: IndicesService,
47
- private readonly slashingProtection: ISlashingProtection,
48
- private readonly processShutdownCallback: ProcessShutdownCallback,
49
- private readonly metrics: Metrics | null
50
- ) {
51
- this.clock.runEveryEpoch(this.pollLiveness);
52
-
53
- if (metrics) {
54
- metrics.doppelganger.statusCount.addCollect(() => this.onScrapeMetrics(metrics));
55
- }
56
-
57
- this.logger.info("Doppelganger protection enabled", {detectionEpochs: DEFAULT_REMAINING_DETECTION_EPOCHS});
58
- }
59
-
60
- async registerValidator(pubkeyHex: PubkeyHex): Promise<void> {
61
- const {currentEpoch} = this.clock;
62
- // Disable doppelganger protection when the validator was initialized before genesis.
63
- // There's no activity before genesis, so doppelganger is pointless.
64
- let remainingEpochs = currentEpoch <= 0 ? REMAINING_EPOCHS_IF_SKIPPED : DEFAULT_REMAINING_DETECTION_EPOCHS;
65
- const nextEpochToCheck = currentEpoch + 1;
66
-
67
- // Log here to alert that validation won't be active until remainingEpochs == 0
68
- if (remainingEpochs > 0) {
69
- const previousEpoch = currentEpoch - 1;
70
- const attestedInPreviousEpoch = await this.slashingProtection.hasAttestedInEpoch(
71
- fromHex(pubkeyHex),
72
- previousEpoch
73
- );
74
-
75
- if (attestedInPreviousEpoch) {
76
- // It is safe to skip doppelganger detection
77
- // https://github.com/ChainSafe/lodestar/issues/5856
78
- remainingEpochs = REMAINING_EPOCHS_IF_SKIPPED;
79
- this.logger.info("Doppelganger detection skipped for validator because restart was detected", {
80
- pubkey: truncBytes(pubkeyHex),
81
- previousEpoch,
82
- });
83
- } else {
84
- this.logger.info("Registered validator for doppelganger detection", {
85
- pubkey: truncBytes(pubkeyHex),
86
- remainingEpochs,
87
- nextEpochToCheck,
88
- });
89
- }
90
- } else {
91
- this.logger.info("Doppelganger detection skipped for validator initialized before genesis", {
92
- pubkey: truncBytes(pubkeyHex),
93
- currentEpoch,
94
- });
95
- }
96
-
97
- this.doppelgangerStateByPubkey.set(pubkeyHex, {
98
- nextEpochToCheck,
99
- remainingEpochs,
100
- });
101
- }
102
-
103
- unregisterValidator(pubkeyHex: PubkeyHex): void {
104
- this.doppelgangerStateByPubkey.delete(pubkeyHex);
105
- }
106
-
107
- getStatus(pubKeyHex: PubkeyHex): DoppelgangerStatus {
108
- return getStatus(this.doppelgangerStateByPubkey.get(pubKeyHex));
109
- }
110
-
111
- isDoppelgangerSafe(pubKeyHex: PubkeyHex): boolean {
112
- return getStatus(this.doppelgangerStateByPubkey.get(pubKeyHex)) === DoppelgangerStatus.VerifiedSafe;
113
- }
114
-
115
- private pollLiveness = async (currentEpoch: Epoch, signal: AbortSignal): Promise<void> => {
116
- if (currentEpoch < 0) {
117
- return;
118
- }
119
-
120
- const endSlotOfCurrentEpoch = computeStartSlotAtEpoch(currentEpoch + 1) - 1;
121
- // Run the doppelganger protection check 75% through the last slot of this epoch. This
122
- // *should* mean that the BN has seen the blocks and attestations for the epoch
123
- await sleep(this.clock.msToSlot(endSlotOfCurrentEpoch + 3 / 4), signal);
124
-
125
- // Collect indices that still need doppelganger checks
126
- const pubkeysToCheckWithoutIndex: PubkeyHex[] = [];
127
- // Collect as Map for detectDoppelganger() which needs to map back index -> pubkey
128
- const indicesToCheckMap = new Map<ValidatorIndex, PubkeyHex>();
129
-
130
- for (const [pubkeyHex, state] of this.doppelgangerStateByPubkey.entries()) {
131
- if (state.remainingEpochs > 0 && state.nextEpochToCheck <= currentEpoch) {
132
- const index = this.indicesService.pubkey2index.get(pubkeyHex);
133
- if (index !== undefined) {
134
- indicesToCheckMap.set(index, pubkeyHex);
135
- } else {
136
- pubkeysToCheckWithoutIndex.push(pubkeyHex);
137
- }
138
- }
139
- }
140
-
141
- // Attempt to collect missing indexes
142
- const newIndices = await this.indicesService.pollValidatorIndices(pubkeysToCheckWithoutIndex);
143
- for (const index of newIndices) {
144
- const pubkey = this.indicesService.index2pubkey.get(index);
145
- if (pubkey) {
146
- indicesToCheckMap.set(index, pubkey);
147
- }
148
- }
149
-
150
- if (indicesToCheckMap.size === 0) {
151
- return;
152
- }
153
-
154
- this.logger.info("Doppelganger liveness check", {currentEpoch, indicesCount: indicesToCheckMap.size});
155
-
156
- // in the current epoch also request for liveness check for past epoch in case a validator index was live
157
- // in the remaining 25% of the last slot of the previous epoch
158
- const indicesToCheck = Array.from(indicesToCheckMap.keys());
159
- const [previous, current] = await Promise.all([
160
- this.getLiveness(currentEpoch - 1, indicesToCheck),
161
- this.getLiveness(currentEpoch, indicesToCheck),
162
- ]);
163
-
164
- this.detectDoppelganger(currentEpoch, previous, current, indicesToCheckMap);
165
- };
166
-
167
- private async getLiveness(epoch: Epoch, indicesToCheck: ValidatorIndex[]): Promise<EpochLivenessData> {
168
- if (epoch < 0) {
169
- return {epoch, responses: []};
170
- }
171
-
172
- const res = await this.api.validator.getLiveness({epoch, indices: indicesToCheck});
173
- if (!res.ok) {
174
- this.logger.error(`Error getting liveness data for epoch ${epoch}`, {}, res.error() as Error);
175
- return {epoch, responses: []};
176
- }
177
- return {epoch, responses: res.value()};
178
- }
179
-
180
- private detectDoppelganger(
181
- currentEpoch: Epoch,
182
- previousEpochLiveness: EpochLivenessData,
183
- currentEpochLiveness: EpochLivenessData,
184
- indicesToCheckMap: Map<ValidatorIndex, PubkeyHex>
185
- ): void {
186
- const previousEpoch = currentEpoch - 1;
187
- const violators: ValidatorIndex[] = [];
188
-
189
- // Perform a loop through the current and previous epoch responses and detect any violators.
190
- //
191
- // A following loop will update the states of each validator, depending on whether or not
192
- // any violators were detected here.
193
-
194
- for (const {epoch, responses} of [previousEpochLiveness, currentEpochLiveness]) {
195
- for (const response of responses) {
196
- if (!response.isLive) {
197
- continue;
198
- }
199
-
200
- const state = this.doppelgangerStateByPubkey.get(indicesToCheckMap.get(response.index) ?? "");
201
- if (!state) {
202
- this.logger.error(`Inconsistent livenessResponseData unknown index ${response.index}`);
203
- continue;
204
- }
205
-
206
- if (state.nextEpochToCheck <= epoch) {
207
- // Doppelganger detected
208
- violators.push(response.index);
209
- }
210
- }
211
- }
212
-
213
- if (violators.length > 0) {
214
- // If a single doppelganger is detected, enable doppelganger checks on all validators forever
215
- for (const state of this.doppelgangerStateByPubkey.values()) {
216
- state.remainingEpochs = REMAINING_EPOCHS_IF_DOPPELGANGER;
217
- }
218
-
219
- this.logger.error(
220
- `Doppelganger(s) detected
221
- A doppelganger occurs when two different validator clients run the same public key.
222
- This validator client detected another instance of a local validator on the network
223
- and is shutting down to prevent potential slashable offenses. Ensure that you are not
224
- running a duplicate or overlapping validator client`,
225
- violators
226
- );
227
-
228
- // Request process to shutdown
229
- this.processShutdownCallback(Error("Doppelganger(s) detected"));
230
- }
231
-
232
- // If not there are no validators
233
- else {
234
- // Iterate through all the previous epoch responses, updating `self.doppelganger_states`.
235
- //
236
- // Do not bother iterating through the current epoch responses since they've already been
237
- // checked for violators and they don't result in updating the state.
238
- for (const response of previousEpochLiveness.responses) {
239
- const state = this.doppelgangerStateByPubkey.get(indicesToCheckMap.get(response.index) ?? "");
240
- if (!state) {
241
- this.logger.error(`Inconsistent livenessResponseData unknown index ${response.index}`);
242
- continue;
243
- }
244
-
245
- if (!response.isLive && state.nextEpochToCheck <= previousEpoch) {
246
- state.remainingEpochs--;
247
- state.nextEpochToCheck = currentEpoch;
248
- this.metrics?.doppelganger.epochsChecked.inc(1);
249
-
250
- const {remainingEpochs, nextEpochToCheck} = state;
251
- if (remainingEpochs <= 0) {
252
- this.logger.info("Doppelganger detection complete", {index: response.index, epoch: currentEpoch});
253
- } else {
254
- this.logger.info("Found no doppelganger", {index: response.index, remainingEpochs, nextEpochToCheck});
255
- }
256
- }
257
- }
258
- }
259
- }
260
-
261
- private onScrapeMetrics(metrics: Metrics): void {
262
- const countByStatus = new Map<DoppelgangerStatus, number>();
263
- for (const state of this.doppelgangerStateByPubkey.values()) {
264
- const status = getStatus(state);
265
- countByStatus.set(status, (countByStatus.get(status) ?? 0) + 1);
266
- }
267
-
268
- // Loop over DoppelgangerStatus not countByStatus to zero status without counts
269
- for (const status of Object.values(DoppelgangerStatus)) {
270
- metrics.doppelganger.statusCount.set({status}, countByStatus.get(status) ?? 0);
271
- }
272
- }
273
- }
274
-
275
- function getStatus(state: DoppelgangerState | undefined): DoppelgangerStatus {
276
- if (!state) {
277
- return DoppelgangerStatus.Unknown;
278
- }
279
- if (state.remainingEpochs <= 0) {
280
- return DoppelgangerStatus.VerifiedSafe;
281
- }
282
- if (state.remainingEpochs === REMAINING_EPOCHS_IF_DOPPELGANGER) {
283
- return DoppelgangerStatus.DoppelgangerDetected;
284
- }
285
- return DoppelgangerStatus.Unverified;
286
- }
@@ -1,43 +0,0 @@
1
- import {EventEmitter} from "node:events";
2
- import {StrictEventEmitter} from "strict-event-emitter-types";
3
- import {Slot} from "@lodestar/types";
4
- import {HeadEventData} from "./chainHeaderTracker.js";
5
-
6
- export enum ValidatorEvent {
7
- /**
8
- * This event signals that the node chain has a new head.
9
- */
10
- chainHead = "chainHead",
11
- }
12
-
13
- export type ValidatorEvents = {
14
- [ValidatorEvent.chainHead]: (head: HeadEventData) => void;
15
- };
16
-
17
- /**
18
- * Emit important validator events.
19
- */
20
- export class ValidatorEventEmitter extends (EventEmitter as {
21
- new (): StrictEventEmitter<EventEmitter, ValidatorEvents>;
22
- }) {
23
- /**
24
- * Wait for the first block to come with slot >= provided slot.
25
- */
26
- async waitForBlockSlot(slot: Slot): Promise<void> {
27
- let headListener: (head: HeadEventData) => void;
28
-
29
- const onDone = (): void => {
30
- this.off(ValidatorEvent.chainHead, headListener);
31
- };
32
-
33
- return new Promise((resolve) => {
34
- headListener = (head: HeadEventData): void => {
35
- if (head.slot >= slot) {
36
- onDone();
37
- resolve();
38
- }
39
- };
40
- this.on(ValidatorEvent.chainHead, headListener);
41
- });
42
- }
43
- }