@lodestar/validator 1.35.0-dev.e9dd48f165 → 1.35.0-dev.f2a741bbe4

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 +18 -2
  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,362 @@
1
+ import {ApiClient, routes} from "@lodestar/api";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {ForkName, isForkPostElectra} from "@lodestar/params";
4
+ import {computeEpochAtSlot, isAggregatorFromCommitteeLength} from "@lodestar/state-transition";
5
+ import {BLSSignature, SignedAggregateAndProof, SingleAttestation, Slot, phase0, ssz} from "@lodestar/types";
6
+ import {prettyBytes, sleep, toRootHex} from "@lodestar/utils";
7
+ import {Metrics} from "../metrics.js";
8
+ import {PubkeyHex} from "../types.js";
9
+ import {IClock, LoggerVc} from "../util/index.js";
10
+ import {AttDutyAndProof, AttestationDutiesService} from "./attestationDuties.js";
11
+ import {ChainHeaderTracker} from "./chainHeaderTracker.js";
12
+ import {ValidatorEventEmitter} from "./emitter.js";
13
+ import {SyncingStatusTracker} from "./syncingStatusTracker.js";
14
+ import {groupAttDutiesByCommitteeIndex} from "./utils.js";
15
+ import {ValidatorStore} from "./validatorStore.js";
16
+
17
+ export type AttestationServiceOpts = {
18
+ afterBlockDelaySlotFraction?: number;
19
+ distributedAggregationSelection?: boolean;
20
+ };
21
+
22
+ /**
23
+ * Previously, submitting attestations too early may cause some attestations missed (because some clients may not queue attestations, and
24
+ * sent peers are few) so it was configured as 1/6. See https://github.com/ChainSafe/lodestar/issues/3943
25
+ *
26
+ * As of Nov 2022, it's proved that submitting attestations asap is better as it avoids busy time of node at around 1/3 of slot (and could be
27
+ * because sent peers are better than before). See https://github.com/ChainSafe/lodestar/issues/4600#issuecomment-1321546586
28
+ */
29
+ const DEFAULT_AFTER_BLOCK_DELAY_SLOT_FRACTION = 0;
30
+
31
+ /**
32
+ * Service that sets up and handles validator attester duties.
33
+ */
34
+ export class AttestationService {
35
+ private readonly dutiesService: AttestationDutiesService;
36
+
37
+ constructor(
38
+ private readonly logger: LoggerVc,
39
+ private readonly api: ApiClient,
40
+ private readonly clock: IClock,
41
+ private readonly validatorStore: ValidatorStore,
42
+ private readonly emitter: ValidatorEventEmitter,
43
+ chainHeadTracker: ChainHeaderTracker,
44
+ syncingStatusTracker: SyncingStatusTracker,
45
+ private readonly metrics: Metrics | null,
46
+ private readonly config: ChainForkConfig,
47
+ private readonly opts?: AttestationServiceOpts
48
+ ) {
49
+ this.dutiesService = new AttestationDutiesService(
50
+ logger,
51
+ api,
52
+ clock,
53
+ validatorStore,
54
+ chainHeadTracker,
55
+ syncingStatusTracker,
56
+ metrics,
57
+ {
58
+ distributedAggregationSelection: opts?.distributedAggregationSelection,
59
+ }
60
+ );
61
+
62
+ // At most every slot, check existing duties from AttestationDutiesService and run tasks
63
+ clock.runEverySlot(this.runAttestationTasks);
64
+ }
65
+
66
+ removeDutiesForKey(pubkey: PubkeyHex): void {
67
+ this.dutiesService.removeDutiesForKey(pubkey);
68
+ }
69
+
70
+ private runAttestationTasks = async (slot: Slot, signal: AbortSignal): Promise<void> => {
71
+ // Fetch info first so a potential delay is absorbed by the sleep() below
72
+ const duties = this.dutiesService.getDutiesAtSlot(slot);
73
+ if (duties.length === 0) {
74
+ return;
75
+ }
76
+ const fork = this.config.getForkName(slot);
77
+
78
+ if (this.opts?.distributedAggregationSelection) {
79
+ // Validator in distributed cluster only has a key share, not the full private key.
80
+ // The partial selection proofs must be exchanged for combined selection proofs by
81
+ // calling submitBeaconCommitteeSelections on the distributed validator middleware client.
82
+ // This will run in parallel to other attestation tasks but must be finished before starting
83
+ // attestation aggregation as it is required to correctly determine if validator is aggregator
84
+ // and to produce a AggregateAndProof that can be threshold aggregated by the middleware client.
85
+ this.runDistributedAggregationSelectionTasks(fork, duties, slot, signal).catch((e) =>
86
+ this.logger.error("Error on attestation aggregation selection", {slot}, e)
87
+ );
88
+ }
89
+
90
+ // A validator should create and broadcast the attestation to the associated attestation subnet when either
91
+ // (a) the validator has received a valid block from the expected block proposer for the assigned slot or
92
+ // (b) ATTESTATION_DUE_BPS of the slot has transpired -- whichever comes first.
93
+ const attestationDueMs = this.config.getAttestationDueMs(fork);
94
+ await Promise.race([
95
+ sleep(attestationDueMs - this.clock.msFromSlot(slot), signal),
96
+ this.emitter.waitForBlockSlot(slot),
97
+ ]);
98
+ this.metrics?.attesterStepCallProduceAttestation.observe(this.clock.secFromSlot(slot) - attestationDueMs / 1000);
99
+
100
+ // Beacon node's endpoint produceAttestationData return data is not dependent on committeeIndex.
101
+ // Produce a single attestation for all committees and submit unaggregated attestations in one go.
102
+ try {
103
+ // Produce a single attestation for all committees, and clone mutate before signing
104
+ const attestationNoCommittee = await this.produceAttestation(0, slot);
105
+
106
+ // Step 1. Mutate, and sign `Attestation` for each validator. Then publish all `Attestations` in one go
107
+ await this.signAndPublishAttestations(fork, slot, attestationNoCommittee, duties);
108
+
109
+ // Step 2. after all attestations are submitted, make an aggregate.
110
+ // First, wait until the `aggregation_production_instant` (AGGREGATE_DUE_BPS of the way through the slot)
111
+ const aggregateDueMs = this.config.getAggregateDueMs(fork);
112
+ await sleep(aggregateDueMs - this.clock.msFromSlot(slot), signal);
113
+ this.metrics?.attesterStepCallProduceAggregate.observe(this.clock.secFromSlot(slot) - aggregateDueMs / 1000);
114
+
115
+ const dutiesByCommitteeIndex = groupAttDutiesByCommitteeIndex(duties);
116
+
117
+ // Then download, sign and publish a `SignedAggregateAndProof` for each
118
+ // validator that is elected to aggregate for this `slot` and `committeeIndex`.
119
+ await Promise.all(
120
+ Array.from(dutiesByCommitteeIndex.entries()).map(([index, dutiesSameCommittee]) => {
121
+ const attestationData: phase0.AttestationData = {
122
+ ...attestationNoCommittee,
123
+ index: isForkPostElectra(fork) ? 0 : index,
124
+ };
125
+ return this.produceAndPublishAggregates(fork, attestationData, index, dutiesSameCommittee);
126
+ })
127
+ );
128
+ } catch (e) {
129
+ this.logger.error("Error on attestation routine", {slot}, e as Error);
130
+ }
131
+ };
132
+
133
+ /**
134
+ * Performs the first step of the attesting process: downloading one `Attestation` object.
135
+ * Beacon node's endpoint produceAttestationData return data is not dependent on committeeIndex.
136
+ * For a validator client with many validators this allows to do a single call for all committees
137
+ * in a slot, saving resources in both the vc and beacon node
138
+ */
139
+ private async produceAttestation(committeeIndex: number, slot: Slot): Promise<phase0.AttestationData> {
140
+ // Produce one attestation data per slot and committeeIndex
141
+ return (await this.api.validator.produceAttestationData({committeeIndex, slot})).value();
142
+ }
143
+
144
+ /**
145
+ * Only one `Attestation` is downloaded from the BN. It is then signed by each
146
+ * validator and the list of individually-signed `Attestation` objects is returned to the BN.
147
+ */
148
+ private async signAndPublishAttestations(
149
+ fork: ForkName,
150
+ slot: Slot,
151
+ attestationNoCommittee: phase0.AttestationData,
152
+ duties: AttDutyAndProof[]
153
+ ): Promise<void> {
154
+ const signedAttestations: SingleAttestation[] = [];
155
+ const headRootHex = toRootHex(attestationNoCommittee.beaconBlockRoot);
156
+ const currentEpoch = computeEpochAtSlot(slot);
157
+
158
+ await Promise.all(
159
+ duties.map(async ({duty}) => {
160
+ const index = isForkPostElectra(fork) ? 0 : duty.committeeIndex;
161
+ const attestationData: phase0.AttestationData = {...attestationNoCommittee, index};
162
+ const logCtxValidator = {slot, index, head: headRootHex, validatorIndex: duty.validatorIndex};
163
+
164
+ try {
165
+ signedAttestations.push(await this.validatorStore.signAttestation(duty, attestationData, currentEpoch));
166
+ this.logger.debug("Signed attestation", logCtxValidator);
167
+ } catch (e) {
168
+ this.metrics?.attestaterError.inc({error: "sign"});
169
+ this.logger.error("Error signing attestation", logCtxValidator, e as Error);
170
+ }
171
+ })
172
+ );
173
+
174
+ // signAndPublishAttestations() may be called before the ATTESTATION_DUE_BPS cutoff time if the block was received early.
175
+ // If we produced the block or we got the block sooner than our peers, our attestations can be dropped because
176
+ // they reach our peers before the block. To prevent that, we wait 2 extra seconds AFTER block arrival, but
177
+ // never beyond the ATTESTATION_DUE_BPS cutoff time.
178
+ // https://github.com/status-im/nimbus-eth2/blob/7b64c1dce4392731a4a59ee3a36caef2e0a8357a/beacon_chain/validators/validator_duties.nim#L1123
179
+ const attestationDueMs = this.config.getAttestationDueMs(fork);
180
+ const msToCutoffTime = attestationDueMs - this.clock.msFromSlot(slot);
181
+ // submitting attestations asap to avoid busy time at around ATTESTATION_DUE_BPS of slot
182
+ const afterBlockDelayMs =
183
+ 1000 *
184
+ this.clock.secondsPerSlot *
185
+ (this.opts?.afterBlockDelaySlotFraction ?? DEFAULT_AFTER_BLOCK_DELAY_SLOT_FRACTION);
186
+ await sleep(Math.min(msToCutoffTime, afterBlockDelayMs));
187
+
188
+ this.metrics?.attesterStepCallPublishAttestation.observe(this.clock.secFromSlot(slot) - attestationDueMs / 1000);
189
+
190
+ // Step 2. Publish all `Attestations` in one go
191
+ try {
192
+ (await this.api.beacon.submitPoolAttestationsV2({signedAttestations})).assertOk();
193
+ this.logger.info("Published attestations", {
194
+ slot,
195
+ head: prettyBytes(headRootHex),
196
+ count: signedAttestations.length,
197
+ });
198
+ this.metrics?.publishedAttestations.inc(signedAttestations.length);
199
+ } catch (e) {
200
+ // Note: metric counts only 1 since we don't know how many signedAttestations are invalid
201
+ this.metrics?.attestaterError.inc({error: "publish"});
202
+ this.logger.error("Error publishing attestations", {slot}, e as Error);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Performs the second step of the attesting process: downloading an aggregated `Attestation`,
208
+ * converting it into a `SignedAggregateAndProof` and returning it to the BN.
209
+ *
210
+ * https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#broadcast-aggregate
211
+ *
212
+ * Only one aggregated `Attestation` is downloaded from the BN. It is then signed
213
+ * by each validator and the list of individually-signed `SignedAggregateAndProof` objects is
214
+ * returned to the BN.
215
+ */
216
+ private async produceAndPublishAggregates(
217
+ fork: ForkName,
218
+ attestation: phase0.AttestationData,
219
+ committeeIndex: number,
220
+ duties: AttDutyAndProof[]
221
+ ): Promise<void> {
222
+ const logCtx = {slot: attestation.slot, index: committeeIndex};
223
+
224
+ // No validator is aggregator, skip
225
+ if (duties.every(({selectionProof}) => selectionProof === null)) {
226
+ return;
227
+ }
228
+
229
+ this.logger.verbose("Aggregating attestations", logCtx);
230
+ const aggregate = (
231
+ await this.api.validator.getAggregatedAttestationV2({
232
+ attestationDataRoot: ssz.phase0.AttestationData.hashTreeRoot(attestation),
233
+ slot: attestation.slot,
234
+ committeeIndex,
235
+ })
236
+ ).value();
237
+ const participants = aggregate.aggregationBits.getTrueBitIndexes().length;
238
+ this.metrics?.numParticipantsInAggregate.observe(participants);
239
+
240
+ const signedAggregateAndProofs: SignedAggregateAndProof[] = [];
241
+
242
+ await Promise.all(
243
+ duties.map(async ({duty, selectionProof}) => {
244
+ const logCtxValidator = {...logCtx, validatorIndex: duty.validatorIndex};
245
+ try {
246
+ // Produce signed aggregates only for validators that are subscribed aggregators.
247
+ if (selectionProof !== null) {
248
+ signedAggregateAndProofs.push(
249
+ await this.validatorStore.signAggregateAndProof(duty, selectionProof, aggregate)
250
+ );
251
+ this.logger.debug("Signed aggregateAndProofs", logCtxValidator);
252
+ }
253
+ } catch (e) {
254
+ this.logger.error("Error signing aggregateAndProofs", logCtxValidator, e as Error);
255
+ }
256
+ })
257
+ );
258
+
259
+ this.metrics?.attesterStepCallPublishAggregate.observe(
260
+ this.clock.secFromSlot(attestation.slot) - this.config.getAggregateDueMs(fork) / 1000
261
+ );
262
+
263
+ if (signedAggregateAndProofs.length > 0) {
264
+ try {
265
+ (await this.api.validator.publishAggregateAndProofsV2({signedAggregateAndProofs})).assertOk();
266
+ this.logger.info("Published aggregateAndProofs", {
267
+ ...logCtx,
268
+ participants,
269
+ count: signedAggregateAndProofs.length,
270
+ });
271
+ this.metrics?.publishedAggregates.inc(signedAggregateAndProofs.length);
272
+ } catch (e) {
273
+ this.logger.error("Error publishing aggregateAndProofs", logCtx, e as Error);
274
+ }
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Performs additional attestation aggregation tasks required if validator is part of distributed cluster
280
+ *
281
+ * 1. Exchange partial for combined selection proofs
282
+ * 2. Determine validators that should aggregate attestations
283
+ * 3. Mutate duty objects to set selection proofs for aggregators
284
+ * 4. Resubscribe validators as aggregators on beacon committee subnets
285
+ *
286
+ * See https://docs.google.com/document/d/1q9jOTPcYQa-3L8luRvQJ-M0eegtba4Nmon3dpO79TMk/mobilebasic
287
+ */
288
+ private async runDistributedAggregationSelectionTasks(
289
+ fork: ForkName,
290
+ duties: AttDutyAndProof[],
291
+ slot: number,
292
+ signal: AbortSignal
293
+ ): Promise<void> {
294
+ const partialSelections: routes.validator.BeaconCommitteeSelection[] = duties.map(
295
+ ({duty, partialSelectionProof}) => ({
296
+ validatorIndex: duty.validatorIndex,
297
+ slot,
298
+ selectionProof: partialSelectionProof as BLSSignature,
299
+ })
300
+ );
301
+
302
+ this.logger.debug("Submitting partial beacon committee selection proofs", {slot, count: partialSelections.length});
303
+
304
+ const res = await Promise.race([
305
+ this.api.validator.submitBeaconCommitteeSelections({selections: partialSelections}),
306
+ // Exit attestation aggregation flow if there is no response after ATTESTATION_DUE_BPS of the slot as
307
+ // beacon node would likely not have enough time to prepare an aggregate attestation.
308
+ // Note that the aggregations flow is not explicitly exited but rather will be skipped
309
+ // due to the fact that calculation of `is_aggregator` in AttestationDutiesService is not done
310
+ // and selectionProof is set to null, meaning no validator will be considered an aggregator.
311
+ sleep(this.config.getAttestationDueMs(fork) - this.clock.msFromSlot(slot), signal),
312
+ ]);
313
+
314
+ if (!res) {
315
+ throw new Error("Failed to receive combined selection proofs before ATTESTATION_DUE_BPS of the slot");
316
+ }
317
+
318
+ const combinedSelections = res.value();
319
+ this.logger.debug("Received combined beacon committee selection proofs", {slot, count: combinedSelections.length});
320
+
321
+ const beaconCommitteeSubscriptions: routes.validator.BeaconCommitteeSubscription[] = [];
322
+
323
+ for (const dutyAndProof of duties) {
324
+ const {validatorIndex, committeeIndex, committeeLength, committeesAtSlot} = dutyAndProof.duty;
325
+ const logCtxValidator = {slot, index: committeeIndex, validatorIndex};
326
+
327
+ const combinedSelection = combinedSelections.find((s) => s.validatorIndex === validatorIndex && s.slot === slot);
328
+
329
+ if (!combinedSelection) {
330
+ this.logger.warn("Did not receive combined beacon committee selection proof", logCtxValidator);
331
+ continue;
332
+ }
333
+
334
+ const isAggregator = isAggregatorFromCommitteeLength(committeeLength, combinedSelection.selectionProof);
335
+
336
+ if (isAggregator) {
337
+ // Update selection proof by mutating duty object
338
+ dutyAndProof.selectionProof = combinedSelection.selectionProof;
339
+
340
+ // Only push subnet subscriptions with `isAggregator=true` as all validators
341
+ // with duties for slot are already subscribed to subnets with `isAggregator=false`.
342
+ beaconCommitteeSubscriptions.push({
343
+ validatorIndex,
344
+ committeesAtSlot,
345
+ committeeIndex,
346
+ slot,
347
+ isAggregator,
348
+ });
349
+ this.logger.debug("Resubscribing validator as aggregator on beacon committee subnet", logCtxValidator);
350
+ }
351
+ }
352
+
353
+ // If there are any subscriptions with aggregators, push them out to the beacon node.
354
+ if (beaconCommitteeSubscriptions.length > 0) {
355
+ (await this.api.validator.prepareBeaconCommitteeSubnet({subscriptions: beaconCommitteeSubscriptions})).assertOk();
356
+ this.logger.debug("Resubscribed validators as aggregators on beacon committee subnets", {
357
+ slot,
358
+ count: beaconCommitteeSubscriptions.length,
359
+ });
360
+ }
361
+ }
362
+ }