@lodestar/validator 1.35.0-dev.c88a6ed255 → 1.35.0-dev.c9deb9b59f

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 (174) hide show
  1. package/lib/buckets.d.ts.map +1 -0
  2. package/lib/defaults.d.ts.map +1 -0
  3. package/lib/genesis.d.ts.map +1 -0
  4. package/lib/index.d.ts +7 -7
  5. package/lib/index.d.ts.map +1 -0
  6. package/lib/index.js +5 -5
  7. package/lib/index.js.map +1 -1
  8. package/lib/metrics.d.ts.map +1 -0
  9. package/lib/metrics.js +14 -14
  10. package/lib/metrics.js.map +1 -1
  11. package/lib/repositories/index.d.ts.map +1 -0
  12. package/lib/repositories/metaDataRepository.d.ts.map +1 -0
  13. package/lib/repositories/metaDataRepository.js +4 -3
  14. package/lib/repositories/metaDataRepository.js.map +1 -1
  15. package/lib/services/attestation.d.ts.map +1 -0
  16. package/lib/services/attestation.js +77 -60
  17. package/lib/services/attestation.js.map +1 -1
  18. package/lib/services/attestationDuties.d.ts.map +1 -0
  19. package/lib/services/attestationDuties.js +105 -98
  20. package/lib/services/attestationDuties.js.map +1 -1
  21. package/lib/services/block.d.ts.map +1 -0
  22. package/lib/services/block.js +64 -56
  23. package/lib/services/block.js.map +1 -1
  24. package/lib/services/blockDuties.d.ts +2 -2
  25. package/lib/services/blockDuties.d.ts.map +1 -0
  26. package/lib/services/blockDuties.js +35 -26
  27. package/lib/services/blockDuties.js.map +1 -1
  28. package/lib/services/chainHeaderTracker.d.ts.map +1 -0
  29. package/lib/services/chainHeaderTracker.js +30 -27
  30. package/lib/services/chainHeaderTracker.js.map +1 -1
  31. package/lib/services/doppelgangerService.d.ts.map +1 -0
  32. package/lib/services/doppelgangerService.js +52 -45
  33. package/lib/services/doppelgangerService.js.map +1 -1
  34. package/lib/services/emitter.d.ts +1 -1
  35. package/lib/services/emitter.d.ts.map +1 -0
  36. package/lib/services/externalSignerSync.d.ts.map +1 -0
  37. package/lib/services/externalSignerSync.js +1 -1
  38. package/lib/services/externalSignerSync.js.map +1 -1
  39. package/lib/services/indices.d.ts.map +1 -0
  40. package/lib/services/indices.js +8 -5
  41. package/lib/services/indices.js.map +1 -1
  42. package/lib/services/prepareBeaconProposer.d.ts.map +1 -0
  43. package/lib/services/prepareBeaconProposer.js.map +1 -1
  44. package/lib/services/syncCommittee.d.ts.map +1 -0
  45. package/lib/services/syncCommittee.js +80 -61
  46. package/lib/services/syncCommittee.js.map +1 -1
  47. package/lib/services/syncCommitteeDuties.d.ts.map +1 -0
  48. package/lib/services/syncCommitteeDuties.js +28 -23
  49. package/lib/services/syncCommitteeDuties.js.map +1 -1
  50. package/lib/services/syncingStatusTracker.d.ts.map +1 -0
  51. package/lib/services/syncingStatusTracker.js +32 -27
  52. package/lib/services/syncingStatusTracker.js.map +1 -1
  53. package/lib/services/utils.d.ts.map +1 -0
  54. package/lib/services/validatorStore.d.ts.map +1 -0
  55. package/lib/services/validatorStore.js +9 -3
  56. package/lib/services/validatorStore.js.map +1 -1
  57. package/lib/slashingProtection/attestation/attestationByTargetRepository.d.ts.map +1 -0
  58. package/lib/slashingProtection/attestation/attestationByTargetRepository.js +7 -3
  59. package/lib/slashingProtection/attestation/attestationByTargetRepository.js.map +1 -1
  60. package/lib/slashingProtection/attestation/attestationLowerBoundRepository.d.ts.map +1 -0
  61. package/lib/slashingProtection/attestation/attestationLowerBoundRepository.js +5 -3
  62. package/lib/slashingProtection/attestation/attestationLowerBoundRepository.js.map +1 -1
  63. package/lib/slashingProtection/attestation/errors.d.ts.map +1 -0
  64. package/lib/slashingProtection/attestation/index.d.ts.map +1 -0
  65. package/lib/slashingProtection/attestation/index.js +3 -0
  66. package/lib/slashingProtection/attestation/index.js.map +1 -1
  67. package/lib/slashingProtection/block/blockBySlotRepository.d.ts.map +1 -0
  68. package/lib/slashingProtection/block/blockBySlotRepository.js +7 -3
  69. package/lib/slashingProtection/block/blockBySlotRepository.js.map +1 -1
  70. package/lib/slashingProtection/block/errors.d.ts.map +1 -0
  71. package/lib/slashingProtection/block/index.d.ts.map +1 -0
  72. package/lib/slashingProtection/block/index.js +1 -0
  73. package/lib/slashingProtection/block/index.js.map +1 -1
  74. package/lib/slashingProtection/index.d.ts +1 -1
  75. package/lib/slashingProtection/index.d.ts.map +1 -0
  76. package/lib/slashingProtection/index.js +3 -0
  77. package/lib/slashingProtection/index.js.map +1 -1
  78. package/lib/slashingProtection/interchange/errors.d.ts.map +1 -0
  79. package/lib/slashingProtection/interchange/formats/completeV4.d.ts.map +1 -0
  80. package/lib/slashingProtection/interchange/formats/index.d.ts.map +1 -0
  81. package/lib/slashingProtection/interchange/formats/v5.d.ts.map +1 -0
  82. package/lib/slashingProtection/interchange/index.d.ts.map +1 -0
  83. package/lib/slashingProtection/interchange/parseInterchange.d.ts.map +1 -0
  84. package/lib/slashingProtection/interchange/serializeInterchange.d.ts.map +1 -0
  85. package/lib/slashingProtection/interchange/types.d.ts.map +1 -0
  86. package/lib/slashingProtection/interface.d.ts.map +1 -0
  87. package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.d.ts.map +1 -0
  88. package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.js +8 -0
  89. package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.js.map +1 -1
  90. package/lib/slashingProtection/minMaxSurround/errors.d.ts.map +1 -0
  91. package/lib/slashingProtection/minMaxSurround/index.d.ts.map +1 -0
  92. package/lib/slashingProtection/minMaxSurround/interface.d.ts.map +1 -0
  93. package/lib/slashingProtection/minMaxSurround/minMaxSurround.d.ts.map +1 -0
  94. package/lib/slashingProtection/minMaxSurround/minMaxSurround.js +2 -0
  95. package/lib/slashingProtection/minMaxSurround/minMaxSurround.js.map +1 -1
  96. package/lib/slashingProtection/types.d.ts.map +1 -0
  97. package/lib/slashingProtection/utils.d.ts +1 -1
  98. package/lib/slashingProtection/utils.d.ts.map +1 -0
  99. package/lib/types.d.ts.map +1 -0
  100. package/lib/util/batch.d.ts.map +1 -0
  101. package/lib/util/clock.d.ts +3 -0
  102. package/lib/util/clock.d.ts.map +1 -0
  103. package/lib/util/clock.js +16 -9
  104. package/lib/util/clock.js.map +1 -1
  105. package/lib/util/difference.d.ts.map +1 -0
  106. package/lib/util/externalSignerClient.d.ts.map +1 -0
  107. package/lib/util/format.d.ts.map +1 -0
  108. package/lib/util/index.d.ts.map +1 -0
  109. package/lib/util/logger.d.ts.map +1 -0
  110. package/lib/util/params.d.ts.map +1 -0
  111. package/lib/util/params.js +17 -1
  112. package/lib/util/params.js.map +1 -1
  113. package/lib/util/url.d.ts.map +1 -0
  114. package/lib/validator.d.ts.map +1 -0
  115. package/lib/validator.js +16 -1
  116. package/lib/validator.js.map +1 -1
  117. package/package.json +19 -16
  118. package/src/buckets.ts +30 -0
  119. package/src/defaults.ts +8 -0
  120. package/src/genesis.ts +19 -0
  121. package/src/index.ts +22 -0
  122. package/src/metrics.ts +417 -0
  123. package/src/repositories/index.ts +1 -0
  124. package/src/repositories/metaDataRepository.ts +42 -0
  125. package/src/services/attestation.ts +362 -0
  126. package/src/services/attestationDuties.ts +406 -0
  127. package/src/services/block.ts +261 -0
  128. package/src/services/blockDuties.ts +217 -0
  129. package/src/services/chainHeaderTracker.ts +89 -0
  130. package/src/services/doppelgangerService.ts +286 -0
  131. package/src/services/emitter.ts +43 -0
  132. package/src/services/externalSignerSync.ts +81 -0
  133. package/src/services/indices.ts +165 -0
  134. package/src/services/prepareBeaconProposer.ts +119 -0
  135. package/src/services/syncCommittee.ts +338 -0
  136. package/src/services/syncCommitteeDuties.ts +337 -0
  137. package/src/services/syncingStatusTracker.ts +74 -0
  138. package/src/services/utils.ts +58 -0
  139. package/src/services/validatorStore.ts +830 -0
  140. package/src/slashingProtection/attestation/attestationByTargetRepository.ts +77 -0
  141. package/src/slashingProtection/attestation/attestationLowerBoundRepository.ts +44 -0
  142. package/src/slashingProtection/attestation/errors.ts +66 -0
  143. package/src/slashingProtection/attestation/index.ts +171 -0
  144. package/src/slashingProtection/block/blockBySlotRepository.ts +78 -0
  145. package/src/slashingProtection/block/errors.ts +28 -0
  146. package/src/slashingProtection/block/index.ts +94 -0
  147. package/src/slashingProtection/index.ts +95 -0
  148. package/src/slashingProtection/interchange/errors.ts +15 -0
  149. package/src/slashingProtection/interchange/formats/completeV4.ts +125 -0
  150. package/src/slashingProtection/interchange/formats/index.ts +7 -0
  151. package/src/slashingProtection/interchange/formats/v5.ts +120 -0
  152. package/src/slashingProtection/interchange/index.ts +5 -0
  153. package/src/slashingProtection/interchange/parseInterchange.ts +55 -0
  154. package/src/slashingProtection/interchange/serializeInterchange.ts +35 -0
  155. package/src/slashingProtection/interchange/types.ts +18 -0
  156. package/src/slashingProtection/interface.ts +28 -0
  157. package/src/slashingProtection/minMaxSurround/distanceStoreRepository.ts +57 -0
  158. package/src/slashingProtection/minMaxSurround/errors.ts +27 -0
  159. package/src/slashingProtection/minMaxSurround/index.ts +4 -0
  160. package/src/slashingProtection/minMaxSurround/interface.ts +23 -0
  161. package/src/slashingProtection/minMaxSurround/minMaxSurround.ts +104 -0
  162. package/src/slashingProtection/types.ts +12 -0
  163. package/src/slashingProtection/utils.ts +42 -0
  164. package/src/types.ts +31 -0
  165. package/src/util/batch.ts +15 -0
  166. package/src/util/clock.ts +169 -0
  167. package/src/util/difference.ts +10 -0
  168. package/src/util/externalSignerClient.ts +277 -0
  169. package/src/util/format.ts +3 -0
  170. package/src/util/index.ts +6 -0
  171. package/src/util/logger.ts +51 -0
  172. package/src/util/params.ts +320 -0
  173. package/src/util/url.ts +16 -0
  174. package/src/validator.ts +418 -0
@@ -0,0 +1,406 @@
1
+ import {ApiClient, routes} from "@lodestar/api";
2
+ import {SLOTS_PER_EPOCH} from "@lodestar/params";
3
+ import {computeEpochAtSlot, isAggregatorFromCommitteeLength, isStartSlotOfEpoch} from "@lodestar/state-transition";
4
+ import {BLSSignature, Epoch, RootHex, Slot, ValidatorIndex} 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, batchItems} 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
+ /**
17
+ * This is to prevent the "Request body is too large" issue for http post.
18
+ * Typical server accept up to1MB (2 ** 20 bytes) of request body, for example fastify and nginx.
19
+ * A typical subscription request is 107 bytes in length, make it 120 to buffer.
20
+ * This number is Math.floor(2 ** 20 / 120)
21
+ **/
22
+ const SUBSCRIPTIONS_PER_REQUEST = 8738;
23
+
24
+ /** Neatly joins the server-generated `AttesterData` with the locally-generated `selectionProof`. */
25
+ export type AttDutyAndProof = {
26
+ duty: routes.validator.AttesterDuty;
27
+ /** This value is only set to not null if the proof indicates that the validator is an aggregator. */
28
+ selectionProof: BLSSignature | null;
29
+ /** This value will only be set if validator is part of distributed cluster and only has a key share */
30
+ partialSelectionProof?: BLSSignature;
31
+ };
32
+
33
+ // To assist with readability
34
+ type AttDutiesAtEpoch = {dependentRoot: RootHex; dutiesByIndex: Map<ValidatorIndex, AttDutyAndProof>};
35
+
36
+ type AttestationDutiesServiceOpts = {
37
+ distributedAggregationSelection?: boolean;
38
+ };
39
+
40
+ export class AttestationDutiesService {
41
+ /** Maps a validator public key to their duties for each epoch */
42
+ private readonly dutiesByIndexByEpoch = new Map<Epoch, AttDutiesAtEpoch>();
43
+ /**
44
+ * We may receive new dependentRoot of an epoch but it's not the last slot of epoch
45
+ * so we have to wait for getting close to the next epoch to redownload new attesterDuties.
46
+ */
47
+ private readonly pendingDependentRootByEpoch = new Map<Epoch, RootHex>();
48
+
49
+ constructor(
50
+ private readonly logger: LoggerVc,
51
+ private readonly api: ApiClient,
52
+ private clock: IClock,
53
+ private readonly validatorStore: ValidatorStore,
54
+ chainHeadTracker: ChainHeaderTracker,
55
+ syncingStatusTracker: SyncingStatusTracker,
56
+ private readonly metrics: Metrics | null,
57
+ private readonly opts?: AttestationDutiesServiceOpts
58
+ ) {
59
+ // Running this task every epoch is safe since a re-org of two epochs is very unlikely
60
+ // TODO: If the re-org event is reliable consider re-running then
61
+ clock.runEveryEpoch(this.runDutiesTasks);
62
+ clock.runEverySlot(this.prepareForNextEpoch);
63
+ chainHeadTracker.runOnNewHead(this.onNewHead);
64
+ syncingStatusTracker.runOnResynced(async (slot) => {
65
+ // Skip on first slot of epoch since tasks are already scheduled
66
+ if (!isStartSlotOfEpoch(slot)) {
67
+ return this.runDutiesTasks(computeEpochAtSlot(slot));
68
+ }
69
+ });
70
+
71
+ if (metrics) {
72
+ metrics.attesterDutiesCount.addCollect(() => {
73
+ const currentSlot = this.clock.getCurrentSlot();
74
+ let duties = 0;
75
+ let nextDutySlot = null;
76
+ for (const [epoch, attDutiesAtEpoch] of this.dutiesByIndexByEpoch) {
77
+ duties += attDutiesAtEpoch.dutiesByIndex.size;
78
+
79
+ // Epochs are sorted, stop searching once a next duty slot is found
80
+ if (epoch < this.clock.currentEpoch || nextDutySlot !== null) continue;
81
+
82
+ for (const {duty} of attDutiesAtEpoch.dutiesByIndex.values()) {
83
+ // Set next duty slot to the closest future slot found in all duties
84
+ if (duty.slot > currentSlot && (nextDutySlot === null || duty.slot < nextDutySlot)) {
85
+ nextDutySlot = duty.slot;
86
+ }
87
+ }
88
+ }
89
+ metrics.attesterDutiesCount.set(duties);
90
+ metrics.attesterDutiesEpochCount.set(this.dutiesByIndexByEpoch.size);
91
+ if (nextDutySlot !== null) metrics.attesterDutiesNextSlot.set(nextDutySlot);
92
+ });
93
+ }
94
+ }
95
+
96
+ removeDutiesForKey(pubkey: PubkeyHex): void {
97
+ for (const [epoch, attDutiesAtEpoch] of this.dutiesByIndexByEpoch) {
98
+ for (const [vIndex, attDutyAndProof] of attDutiesAtEpoch.dutiesByIndex) {
99
+ if (toPubkeyHex(attDutyAndProof.duty.pubkey) === pubkey) {
100
+ attDutiesAtEpoch.dutiesByIndex.delete(vIndex);
101
+ if (attDutiesAtEpoch.dutiesByIndex.size === 0) {
102
+ this.dutiesByIndexByEpoch.delete(epoch);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ /** Returns all `ValidatorDuty` for the given `slot` */
110
+ getDutiesAtSlot(slot: Slot): AttDutyAndProof[] {
111
+ const epoch = computeEpochAtSlot(slot);
112
+ const duties: AttDutyAndProof[] = [];
113
+ const epochDuties = this.dutiesByIndexByEpoch.get(epoch);
114
+ if (epochDuties === undefined) {
115
+ return duties;
116
+ }
117
+
118
+ for (const validatorDuty of epochDuties.dutiesByIndex.values()) {
119
+ if (validatorDuty.duty.slot === slot) {
120
+ duties.push(validatorDuty);
121
+ }
122
+ }
123
+
124
+ return duties;
125
+ }
126
+
127
+ /**
128
+ * If a reorg dependent root comes at a slot other than last slot of epoch
129
+ * just update this.pendingDependentRootByEpoch() and process here
130
+ */
131
+ private prepareForNextEpoch = async (slot: Slot, signal: AbortSignal): Promise<void> => {
132
+ // only interested in last slot of epoch
133
+ if ((slot + 1) % SLOTS_PER_EPOCH !== 0) {
134
+ return;
135
+ }
136
+
137
+ // TODO GLOAS: re-evaluate this timing
138
+ // during the 1 / 3 of epoch, last block of epoch may come
139
+ await sleep(this.clock.msToSlot(slot + 1 / 3), signal);
140
+
141
+ const nextEpoch = computeEpochAtSlot(slot) + 1;
142
+ const dependentRoot = this.dutiesByIndexByEpoch.get(nextEpoch)?.dependentRoot;
143
+ const pendingDependentRoot = this.pendingDependentRootByEpoch.get(nextEpoch);
144
+ if (dependentRoot && pendingDependentRoot && dependentRoot !== pendingDependentRoot) {
145
+ // this happens when pendingDependentRoot is not the last block of an epoch
146
+ this.logger.info("Redownload attester duties when it's close to epoch boundary", {nextEpoch, slot});
147
+ await this.handleAttesterDutiesReorg(nextEpoch, slot, dependentRoot, pendingDependentRoot);
148
+ }
149
+ };
150
+
151
+ private runDutiesTasks = async (epoch: Epoch): Promise<void> => {
152
+ await Promise.all([
153
+ // Run pollBeaconAttesters immediately for all known local indices
154
+ this.pollBeaconAttesters(epoch, this.validatorStore.getAllLocalIndices()).catch((e: Error) => {
155
+ this.logger.error("Error on poll attesters", {epoch}, e);
156
+ }),
157
+
158
+ // At the same time fetch any remaining unknown validator indices, then poll duties for those newIndices only
159
+ this.validatorStore
160
+ .pollValidatorIndices()
161
+ .then((newIndices) => this.pollBeaconAttesters(epoch, newIndices))
162
+ .catch((e: Error) => {
163
+ this.logger.error("Error on poll indices and attesters", {epoch}, e);
164
+ }),
165
+ ]);
166
+
167
+ // After both, prune
168
+ this.pruneOldDuties(epoch);
169
+ };
170
+
171
+ /**
172
+ * Query the beacon node for attestation duties for any known validators.
173
+ *
174
+ * This function will perform (in the following order):
175
+ *
176
+ * 1. Poll for current-epoch duties and update the local duties map.
177
+ * 2. As above, but for the next-epoch.
178
+ * 3. Push out any attestation subnet subscriptions to the BN.
179
+ * 4. Prune old entries from duties.
180
+ */
181
+ private async pollBeaconAttesters(currentEpoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
182
+ const nextEpoch = currentEpoch + 1;
183
+
184
+ // No need to bother the BN if we don't have any validators.
185
+ if (indexArr.length === 0) {
186
+ return;
187
+ }
188
+
189
+ for (const epoch of [currentEpoch, nextEpoch]) {
190
+ // Download the duties and update the duties for the current and next epoch.
191
+ await this.pollBeaconAttestersForEpoch(epoch, indexArr).catch((e: Error) => {
192
+ this.logger.error("Failed to download attester duties", {epoch}, e);
193
+ });
194
+ }
195
+
196
+ const beaconCommitteeSubscriptions: routes.validator.BeaconCommitteeSubscription[] = [];
197
+
198
+ // For this epoch and the next epoch, produce any beacon committee subscriptions.
199
+ //
200
+ // We are *always* pushing out subscriptions, even if we've subscribed before. This is
201
+ // potentially excessive on the BN in normal cases, but it will help with fast re-subscriptions
202
+ // if the BN goes offline or we swap to a different one.
203
+ const indexSet = new Set(indexArr);
204
+ for (const epoch of [currentEpoch, nextEpoch]) {
205
+ const epochDuties = this.dutiesByIndexByEpoch.get(epoch)?.dutiesByIndex;
206
+ if (epochDuties) {
207
+ for (const {duty, selectionProof} of epochDuties.values()) {
208
+ if (indexSet.has(duty.validatorIndex)) {
209
+ beaconCommitteeSubscriptions.push({
210
+ validatorIndex: duty.validatorIndex,
211
+ committeesAtSlot: duty.committeesAtSlot,
212
+ committeeIndex: duty.committeeIndex,
213
+ slot: duty.slot,
214
+ isAggregator: selectionProof !== null,
215
+ });
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ // If there are any subscriptions, push them out to the beacon node.
222
+ if (beaconCommitteeSubscriptions.length > 0) {
223
+ const subscriptionsBatches = batchItems(beaconCommitteeSubscriptions, {batchSize: SUBSCRIPTIONS_PER_REQUEST});
224
+ const responses = await Promise.all(
225
+ subscriptionsBatches.map((subscriptions) => this.api.validator.prepareBeaconCommitteeSubnet({subscriptions}))
226
+ );
227
+
228
+ for (const res of responses) {
229
+ res.assertOk();
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * For the given `indexArr`, download the duties for the given `epoch` and store them in duties.
236
+ */
237
+ private async pollBeaconAttestersForEpoch(epoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
238
+ // Don't fetch duties for epochs before genesis. However, should fetch epoch 0 duties at epoch -1
239
+ if (epoch < 0) {
240
+ return;
241
+ }
242
+
243
+ const res = await this.api.validator.getAttesterDuties({epoch, indices: indexArr});
244
+ const attesterDuties = res.value();
245
+ const {dependentRoot} = res.meta();
246
+ const relevantDuties = attesterDuties.filter((duty) => {
247
+ const pubkeyHex = toPubkeyHex(duty.pubkey);
248
+ return this.validatorStore.hasVotingPubkey(pubkeyHex) && this.validatorStore.isDoppelgangerSafe(pubkeyHex);
249
+ });
250
+
251
+ this.logger.debug("Downloaded attester duties", {epoch, dependentRoot, count: relevantDuties.length});
252
+
253
+ const dutiesAtEpoch = this.dutiesByIndexByEpoch.get(epoch);
254
+ const priorDependentRoot = dutiesAtEpoch?.dependentRoot;
255
+ const dependentRootChanged = priorDependentRoot !== undefined && priorDependentRoot !== dependentRoot;
256
+
257
+ if (!priorDependentRoot || dependentRootChanged) {
258
+ const dutiesByIndex = new Map<ValidatorIndex, AttDutyAndProof>();
259
+ for (const duty of relevantDuties) {
260
+ const dutyAndProof = await this.getDutyAndProof(duty);
261
+ dutiesByIndex.set(duty.validatorIndex, dutyAndProof);
262
+ }
263
+ this.dutiesByIndexByEpoch.set(epoch, {dependentRoot, dutiesByIndex});
264
+
265
+ if (priorDependentRoot && dependentRootChanged) {
266
+ this.metrics?.attesterDutiesReorg.inc();
267
+ this.logger.warn("Attester duties re-org. This may happen from time to time", {
268
+ priorDependentRoot: priorDependentRoot,
269
+ dependentRoot: dependentRoot,
270
+ epoch,
271
+ });
272
+ }
273
+ } else {
274
+ const existingDuties = dutiesAtEpoch.dutiesByIndex;
275
+ const existingDutiesCount = existingDuties.size;
276
+ const discoveredNewDuties = relevantDuties.length > existingDutiesCount;
277
+
278
+ if (discoveredNewDuties) {
279
+ for (const duty of relevantDuties) {
280
+ if (!existingDuties.has(duty.validatorIndex)) {
281
+ const dutyAndProof = await this.getDutyAndProof(duty);
282
+ existingDuties.set(duty.validatorIndex, dutyAndProof);
283
+ }
284
+ }
285
+
286
+ this.logger.debug("Discovered new attester duties", {
287
+ epoch,
288
+ dependentRoot,
289
+ count: relevantDuties.length - existingDutiesCount,
290
+ });
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * attester duties may be reorged due to 2 scenarios:
297
+ * 1. node is syncing (for nextEpoch duties)
298
+ * 2. node is reorged
299
+ * previousDutyDependentRoot = get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)
300
+ * => dependent root of current epoch
301
+ * currentDutyDependentRoot = get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch) - 1)
302
+ * => dependent root of next epoch
303
+ */
304
+ private onNewHead = async ({
305
+ slot,
306
+ head,
307
+ previousDutyDependentRoot,
308
+ currentDutyDependentRoot,
309
+ }: HeadEventData): Promise<void> => {
310
+ const currentEpoch = computeEpochAtSlot(slot);
311
+ const nextEpoch = currentEpoch + 1;
312
+ const nextTwoEpoch = currentEpoch + 2;
313
+ const nextTwoEpochDependentRoot = this.dutiesByIndexByEpoch.get(currentEpoch + 2)?.dependentRoot;
314
+
315
+ // this may happen ONLY when node is syncing
316
+ // it's safe to get attester duties at epoch n + 1 thanks to nextEpochShuffling cache
317
+ // but it's an issue to request attester duties for epoch n + 2 as dependent root keeps changing while node is syncing
318
+ // see https://github.com/ChainSafe/lodestar/issues/3211
319
+ if (nextTwoEpochDependentRoot && head !== nextTwoEpochDependentRoot) {
320
+ // last slot of epoch, we're sure it's the correct dependent root
321
+ if ((slot + 1) % SLOTS_PER_EPOCH === 0) {
322
+ this.logger.info("Next 2 epoch attester duties reorg", {slot, dutyEpoch: nextTwoEpoch, head});
323
+ await this.handleAttesterDutiesReorg(nextTwoEpoch, slot, nextTwoEpochDependentRoot, head);
324
+ } else {
325
+ this.logger.debug("Potential next 2 epoch attester duties reorg", {slot, dutyEpoch: nextTwoEpoch, head});
326
+ // node may send adjacent onHead events while it's syncing
327
+ // wait for getting close to next epoch to make sure the dependRoot
328
+ this.pendingDependentRootByEpoch.set(nextTwoEpoch, head);
329
+ }
330
+ }
331
+
332
+ // dependent root for next epoch changed
333
+ const nextEpochDependentRoot = this.dutiesByIndexByEpoch.get(nextEpoch)?.dependentRoot;
334
+ if (nextEpochDependentRoot && currentDutyDependentRoot !== nextEpochDependentRoot) {
335
+ this.logger.warn("Potential next epoch attester duties reorg", {
336
+ slot,
337
+ dutyEpoch: nextEpoch,
338
+ priorDependentRoot: nextEpochDependentRoot,
339
+ newDependentRoot: currentDutyDependentRoot,
340
+ });
341
+ await this.handleAttesterDutiesReorg(nextEpoch, slot, nextEpochDependentRoot, currentDutyDependentRoot);
342
+ }
343
+
344
+ // dependent root for current epoch changed
345
+ const currentEpochDependentRoot = this.dutiesByIndexByEpoch.get(currentEpoch)?.dependentRoot;
346
+ if (currentEpochDependentRoot && currentEpochDependentRoot !== previousDutyDependentRoot) {
347
+ this.logger.warn("Potential current epoch attester duties reorg", {
348
+ slot,
349
+ dutyEpoch: currentEpoch,
350
+ priorDependentRoot: currentEpochDependentRoot,
351
+ newDependentRoot: previousDutyDependentRoot,
352
+ });
353
+ await this.handleAttesterDutiesReorg(currentEpoch, slot, currentEpochDependentRoot, previousDutyDependentRoot);
354
+ }
355
+ };
356
+
357
+ private async handleAttesterDutiesReorg(
358
+ dutyEpoch: Epoch,
359
+ slot: Slot,
360
+ oldDependentRoot: RootHex,
361
+ newDependentRoot: RootHex
362
+ ): Promise<void> {
363
+ this.metrics?.attesterDutiesReorg.inc();
364
+ const logContext = {dutyEpoch, slot, oldDependentRoot, newDependentRoot};
365
+ this.logger.debug("Redownload attester duties", logContext);
366
+
367
+ await this.pollBeaconAttestersForEpoch(dutyEpoch, this.validatorStore.getAllLocalIndices())
368
+ .then(() => {
369
+ this.pendingDependentRootByEpoch.delete(dutyEpoch);
370
+ })
371
+ .catch((e: Error) => {
372
+ this.logger.error("Failed to redownload attester duties when reorg happens", logContext, e);
373
+ });
374
+ }
375
+
376
+ private async getDutyAndProof(duty: routes.validator.AttesterDuty): Promise<AttDutyAndProof> {
377
+ const selectionProof = await this.validatorStore.signAttestationSelectionProof(duty.pubkey, duty.slot);
378
+
379
+ if (this.opts?.distributedAggregationSelection) {
380
+ // Validator in distributed cluster only has a key share, not the full private key.
381
+ // Passing a partial selection proof to `is_aggregator` would produce incorrect result.
382
+ // AttestationService will exchange partial for combined selection proofs retrieved from
383
+ // distributed validator middleware client and determine aggregators at beginning of every slot.
384
+ return {duty, selectionProof: null, partialSelectionProof: selectionProof};
385
+ }
386
+
387
+ const isAggregator = isAggregatorFromCommitteeLength(duty.committeeLength, selectionProof);
388
+
389
+ return {
390
+ duty,
391
+ // selectionProof === null is used to check if is aggregator
392
+ selectionProof: isAggregator ? selectionProof : null,
393
+ };
394
+ }
395
+
396
+ /** Run once per epoch to prune duties map */
397
+ private pruneOldDuties(currentEpoch: Epoch): void {
398
+ for (const byEpochMap of [this.dutiesByIndexByEpoch, this.pendingDependentRootByEpoch]) {
399
+ for (const epoch of byEpochMap.keys()) {
400
+ if (epoch + HISTORICAL_DUTIES_EPOCHS < currentEpoch) {
401
+ byEpochMap.delete(epoch);
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
@@ -0,0 +1,261 @@
1
+ import {ApiClient, routes} from "@lodestar/api";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {
4
+ BLSPubkey,
5
+ BLSSignature,
6
+ BeaconBlock,
7
+ BlindedBeaconBlock,
8
+ BlockContents,
9
+ ProducedBlockSource,
10
+ SignedBlindedBeaconBlock,
11
+ SignedBlockContents,
12
+ Slot,
13
+ isBlindedSignedBeaconBlock,
14
+ } from "@lodestar/types";
15
+ import {extendError, prettyBytes, prettyWeiToEth, toPubkeyHex} from "@lodestar/utils";
16
+ import {Metrics} from "../metrics.js";
17
+ import {PubkeyHex} from "../types.js";
18
+ import {IClock, LoggerVc} from "../util/index.js";
19
+ import {BlockDutiesService, GENESIS_SLOT} from "./blockDuties.js";
20
+ import {ValidatorStore} from "./validatorStore.js";
21
+
22
+ // The following combination of blocks and blobs can be produced
23
+ // i) a full block contents (eg block and all related data-layer data)
24
+ // ii) a blinded block post bellatrix
25
+ type BlindedBlockOrBlockContents =
26
+ | {
27
+ blockContents: BlockContents;
28
+ executionPayloadBlinded: false;
29
+ executionPayloadSource: ProducedBlockSource.engine;
30
+ }
31
+ | {
32
+ block: BlindedBeaconBlock;
33
+ executionPayloadBlinded: true;
34
+ executionPayloadSource: ProducedBlockSource;
35
+ };
36
+
37
+ type DebugLogCtx = {debugLogCtx: Record<string, string | boolean | undefined>};
38
+ type BlockProposalOpts = {
39
+ broadcastValidation: routes.beacon.BroadcastValidation;
40
+ blindedLocal: boolean;
41
+ };
42
+ /**
43
+ * Service that sets up and handles validator block proposal duties.
44
+ */
45
+ export class BlockProposingService {
46
+ private readonly dutiesService: BlockDutiesService;
47
+
48
+ constructor(
49
+ private readonly config: ChainForkConfig,
50
+ private readonly logger: LoggerVc,
51
+ private readonly api: ApiClient,
52
+ private readonly clock: IClock,
53
+ private readonly validatorStore: ValidatorStore,
54
+ private readonly metrics: Metrics | null,
55
+ private readonly opts: BlockProposalOpts
56
+ ) {
57
+ this.dutiesService = new BlockDutiesService(
58
+ config,
59
+ logger,
60
+ api,
61
+ clock,
62
+ validatorStore,
63
+ metrics,
64
+ this.notifyBlockProductionFn
65
+ );
66
+ }
67
+
68
+ removeDutiesForKey(pubkey: PubkeyHex): void {
69
+ this.dutiesService.removeDutiesForKey(pubkey);
70
+ }
71
+
72
+ /**
73
+ * `BlockDutiesService` must call this fn to trigger block creation
74
+ * This function may run more than once at a time, rationale in `BlockDutiesService.pollBeaconProposers`
75
+ */
76
+ private notifyBlockProductionFn = (slot: Slot, proposers: BLSPubkey[]): void => {
77
+ if (slot <= GENESIS_SLOT) {
78
+ this.logger.debug("Not producing block before or at genesis slot");
79
+ return;
80
+ }
81
+
82
+ if (proposers.length > 1) {
83
+ this.logger.warn("Multiple block proposers", {slot, count: proposers.length});
84
+ }
85
+
86
+ Promise.all(proposers.map((pubkey) => this.createAndPublishBlock(pubkey, slot))).catch((e: Error) => {
87
+ this.logger.error("Error on block duties", {slot}, e);
88
+ });
89
+ };
90
+
91
+ /** Produce a block at the given slot for pubkey */
92
+ private async createAndPublishBlock(pubkey: BLSPubkey, slot: Slot): Promise<void> {
93
+ const pubkeyHex = toPubkeyHex(pubkey);
94
+ const logCtx = {slot, validator: prettyBytes(pubkeyHex)};
95
+
96
+ // Wrap with try catch here to re-use `logCtx`
97
+ try {
98
+ const randaoReveal = await this.validatorStore.signRandao(pubkey, slot);
99
+ const graffiti = this.validatorStore.getGraffiti(pubkeyHex);
100
+
101
+ const debugLogCtx = {...logCtx, validator: pubkeyHex};
102
+
103
+ const strictFeeRecipientCheck = this.validatorStore.strictFeeRecipientCheck(pubkeyHex);
104
+ const {selection: builderSelection, boostFactor: builderBoostFactor} =
105
+ this.validatorStore.getBuilderSelectionParams(pubkeyHex);
106
+ const feeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex);
107
+ const blindedLocal = this.opts.blindedLocal;
108
+
109
+ this.logger.debug("Producing block", {
110
+ ...debugLogCtx,
111
+ builderSelection,
112
+ builderBoostFactor,
113
+ feeRecipient,
114
+ strictFeeRecipientCheck,
115
+ blindedLocal,
116
+ });
117
+ this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot));
118
+
119
+ const produceOpts = {
120
+ feeRecipient,
121
+ strictFeeRecipientCheck,
122
+ blindedLocal,
123
+ };
124
+ const blockContentsWrapper = await this.produceBlockWrapper(
125
+ this.config,
126
+ slot,
127
+ randaoReveal,
128
+ graffiti,
129
+ builderBoostFactor,
130
+ produceOpts,
131
+ builderSelection
132
+ ).catch((e: Error) => {
133
+ this.metrics?.blockProposingErrors.inc({error: "produce"});
134
+ throw extendError(e, "Failed to produce block");
135
+ });
136
+
137
+ this.logger.debug("Produced block", {...debugLogCtx, ...blockContentsWrapper.debugLogCtx});
138
+ this.metrics?.blocksProduced.inc();
139
+
140
+ const block = blockContentsWrapper.executionPayloadBlinded
141
+ ? blockContentsWrapper.block
142
+ : blockContentsWrapper.blockContents.block;
143
+ const signedBlock = await this.validatorStore.signBlock(pubkey, block, slot, this.logger);
144
+
145
+ const {broadcastValidation} = this.opts;
146
+ const publishOpts = {broadcastValidation};
147
+
148
+ const signedBlindedBlockOrBlockContents = blockContentsWrapper.executionPayloadBlinded
149
+ ? {signedBlock}
150
+ : {signedBlock, ...blockContentsWrapper.blockContents};
151
+ delete (signedBlindedBlockOrBlockContents as {block?: BeaconBlock}).block; // remove block if present
152
+
153
+ await this.publishBlockWrapper(signedBlindedBlockOrBlockContents, publishOpts).catch((e: Error) => {
154
+ this.metrics?.blockProposingErrors.inc({error: "publish"});
155
+ throw extendError(e, "Failed to publish block");
156
+ });
157
+
158
+ this.metrics?.proposerStepCallPublishBlock.observe(this.clock.secFromSlot(slot));
159
+ this.metrics?.blocksPublished.inc();
160
+ this.logger.info("Published block", {...logCtx, graffiti, ...blockContentsWrapper.debugLogCtx});
161
+ } catch (e) {
162
+ this.logger.error("Error proposing block", logCtx, e as Error);
163
+ }
164
+ }
165
+
166
+ private publishBlockWrapper = async (
167
+ signedBlindedBlockOrBlockContents: SignedBlockContents | {signedBlock: SignedBlindedBeaconBlock},
168
+ opts: {broadcastValidation?: routes.beacon.BroadcastValidation} = {}
169
+ ): Promise<void> => {
170
+ if (isBlindedSignedBeaconBlock(signedBlindedBlockOrBlockContents.signedBlock)) {
171
+ (
172
+ await this.api.beacon.publishBlindedBlockV2({
173
+ signedBlindedBlock: signedBlindedBlockOrBlockContents.signedBlock,
174
+ ...opts,
175
+ })
176
+ ).assertOk();
177
+ } else {
178
+ (
179
+ await this.api.beacon.publishBlockV2({
180
+ signedBlockContents: signedBlindedBlockOrBlockContents,
181
+ ...opts,
182
+ })
183
+ ).assertOk();
184
+ }
185
+ };
186
+
187
+ private produceBlockWrapper = async (
188
+ _config: ChainForkConfig,
189
+ slot: Slot,
190
+ randaoReveal: BLSSignature,
191
+ graffiti: string | undefined,
192
+ builderBoostFactor: bigint,
193
+ {feeRecipient, strictFeeRecipientCheck, blindedLocal}: routes.validator.ExtraProduceBlockOpts,
194
+ builderSelection: routes.validator.BuilderSelection
195
+ ): Promise<BlindedBlockOrBlockContents & DebugLogCtx> => {
196
+ const res = await this.api.validator.produceBlockV3({
197
+ slot,
198
+ randaoReveal,
199
+ graffiti,
200
+ skipRandaoVerification: false,
201
+ feeRecipient,
202
+ builderSelection,
203
+ strictFeeRecipientCheck,
204
+ blindedLocal,
205
+ builderBoostFactor,
206
+ });
207
+ const meta = res.meta();
208
+
209
+ const debugLogCtx = {
210
+ executionPayloadSource: meta.executionPayloadSource,
211
+ executionPayloadBlinded: meta.executionPayloadBlinded,
212
+ executionPayloadValue: prettyWeiToEth(meta.executionPayloadValue),
213
+ consensusBlockValue: prettyWeiToEth(meta.consensusBlockValue),
214
+ totalBlockValue: prettyWeiToEth(meta.executionPayloadValue + meta.consensusBlockValue),
215
+ // TODO PR: should be used in api call instead of adding in log
216
+ strictFeeRecipientCheck,
217
+ builderSelection,
218
+ api: "produceBlockV3",
219
+ };
220
+
221
+ return parseProduceBlockResponse({data: res.value(), ...meta}, debugLogCtx, builderSelection);
222
+ };
223
+ }
224
+
225
+ function parseProduceBlockResponse(
226
+ response: {data: BlockContents | BlindedBeaconBlock} & {
227
+ executionPayloadSource: ProducedBlockSource;
228
+ executionPayloadBlinded: boolean;
229
+ },
230
+ debugLogCtx: Record<string, string | boolean | undefined>,
231
+ builderSelection: routes.validator.BuilderSelection
232
+ ): BlindedBlockOrBlockContents & DebugLogCtx {
233
+ const executionPayloadSource = response.executionPayloadSource;
234
+
235
+ if (
236
+ (builderSelection === routes.validator.BuilderSelection.BuilderOnly &&
237
+ executionPayloadSource === ProducedBlockSource.engine) ||
238
+ (builderSelection === routes.validator.BuilderSelection.ExecutionOnly &&
239
+ executionPayloadSource === ProducedBlockSource.builder)
240
+ ) {
241
+ throw Error(
242
+ `Block not produced as per desired builderSelection=${builderSelection} executionPayloadSource=${executionPayloadSource}`
243
+ );
244
+ }
245
+
246
+ if (response.executionPayloadBlinded) {
247
+ return {
248
+ block: response.data,
249
+ executionPayloadBlinded: true,
250
+ executionPayloadSource,
251
+ debugLogCtx,
252
+ } as BlindedBlockOrBlockContents & DebugLogCtx;
253
+ }
254
+
255
+ return {
256
+ blockContents: response.data,
257
+ executionPayloadBlinded: false,
258
+ executionPayloadSource,
259
+ debugLogCtx,
260
+ } as BlindedBlockOrBlockContents & DebugLogCtx;
261
+ }