@lodestar/beacon-node 1.29.0-dev.87d367d9f7 → 1.29.0-dev.8aa1ad8d30

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 (68) hide show
  1. package/lib/api/impl/beacon/pool/index.js +1 -1
  2. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  3. package/lib/api/impl/validator/index.js +5 -4
  4. package/lib/api/impl/validator/index.js.map +1 -1
  5. package/lib/chain/archiveStore/historicalState/worker.js +12 -0
  6. package/lib/chain/archiveStore/historicalState/worker.js.map +1 -1
  7. package/lib/chain/chain.js +5 -7
  8. package/lib/chain/chain.js.map +1 -1
  9. package/lib/chain/opPools/aggregatedAttestationPool.d.ts +32 -14
  10. package/lib/chain/opPools/aggregatedAttestationPool.js +235 -97
  11. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  12. package/lib/chain/opPools/attestationPool.d.ts +4 -2
  13. package/lib/chain/opPools/attestationPool.js +4 -3
  14. package/lib/chain/opPools/attestationPool.js.map +1 -1
  15. package/lib/metrics/metrics/beacon.js +1 -1
  16. package/lib/metrics/metrics/beacon.js.map +1 -1
  17. package/lib/metrics/metrics/lodestar.d.ts +62 -10
  18. package/lib/metrics/metrics/lodestar.js +141 -28
  19. package/lib/metrics/metrics/lodestar.js.map +1 -1
  20. package/lib/metrics/options.d.ts +1 -1
  21. package/lib/metrics/validatorMonitor.js +1 -1
  22. package/lib/metrics/validatorMonitor.js.map +1 -1
  23. package/lib/network/core/networkCore.d.ts +3 -3
  24. package/lib/network/core/networkCore.js +9 -4
  25. package/lib/network/core/networkCore.js.map +1 -1
  26. package/lib/network/core/networkCoreWorker.js +5 -3
  27. package/lib/network/core/networkCoreWorker.js.map +1 -1
  28. package/lib/network/core/networkCoreWorkerHandler.d.ts +2 -2
  29. package/lib/network/core/networkCoreWorkerHandler.js +3 -3
  30. package/lib/network/core/networkCoreWorkerHandler.js.map +1 -1
  31. package/lib/network/core/types.d.ts +1 -1
  32. package/lib/network/discv5/index.d.ts +2 -3
  33. package/lib/network/discv5/index.js +4 -5
  34. package/lib/network/discv5/index.js.map +1 -1
  35. package/lib/network/discv5/types.d.ts +1 -1
  36. package/lib/network/discv5/worker.js +7 -6
  37. package/lib/network/discv5/worker.js.map +1 -1
  38. package/lib/network/gossip/gossipsub.js +4 -0
  39. package/lib/network/gossip/gossipsub.js.map +1 -1
  40. package/lib/network/interface.d.ts +3 -2
  41. package/lib/network/libp2p/index.d.ts +2 -2
  42. package/lib/network/libp2p/index.js +9 -9
  43. package/lib/network/libp2p/index.js.map +1 -1
  44. package/lib/network/network.d.ts +4 -4
  45. package/lib/network/network.js +7 -5
  46. package/lib/network/network.js.map +1 -1
  47. package/lib/network/peers/datastore.d.ts +2 -1
  48. package/lib/network/peers/datastore.js +1 -1
  49. package/lib/network/peers/datastore.js.map +1 -1
  50. package/lib/network/peers/discover.d.ts +2 -0
  51. package/lib/network/peers/discover.js +3 -4
  52. package/lib/network/peers/discover.js.map +1 -1
  53. package/lib/network/peers/peerManager.d.ts +2 -1
  54. package/lib/network/peers/peerManager.js +1 -1
  55. package/lib/network/peers/peerManager.js.map +1 -1
  56. package/lib/network/peers/utils/getConnectedPeerIds.js +2 -2
  57. package/lib/network/peers/utils/getConnectedPeerIds.js.map +1 -1
  58. package/lib/network/processor/gossipHandlers.js +3 -2
  59. package/lib/network/processor/gossipHandlers.js.map +1 -1
  60. package/lib/network/util.d.ts +4 -1
  61. package/lib/network/util.js +2 -2
  62. package/lib/network/util.js.map +1 -1
  63. package/lib/node/nodejs.d.ts +3 -3
  64. package/lib/node/nodejs.js +2 -2
  65. package/lib/node/nodejs.js.map +1 -1
  66. package/lib/util/peerId.js +1 -1
  67. package/lib/util/peerId.js.map +1 -1
  68. package/package.json +31 -32
@@ -1,11 +1,11 @@
1
1
  import { aggregateSignatures } from "@chainsafe/blst";
2
2
  import { BitArray } from "@chainsafe/ssz";
3
- import { EpochDifference } from "@lodestar/fork-choice";
4
3
  import { ForkName, ForkSeq, MAX_ATTESTATIONS, MAX_ATTESTATIONS_ELECTRA, MAX_COMMITTEES_PER_SLOT, MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, isForkPostDeneb, isForkPostElectra, } from "@lodestar/params";
5
4
  import { computeEpochAtSlot, computeSlotsSinceEpochStart, computeStartSlotAtEpoch, getBlockRootAtSlot, } from "@lodestar/state-transition";
6
5
  import { isElectraAttestation, ssz, } from "@lodestar/types";
7
6
  import { assert, MapDef, toRootHex } from "@lodestar/utils";
8
7
  import { IntersectResult, intersectUint8Arrays } from "../../util/bitArray.js";
8
+ import { getShufflingDependentRoot } from "../../util/dependentRoot.js";
9
9
  import { InsertOutcome } from "./types.js";
10
10
  import { pruneBySlot, signatureFromBytesNoCheck } from "./utils.js";
11
11
  /**
@@ -15,6 +15,14 @@ import { pruneBySlot, signatureFromBytesNoCheck } from "./utils.js";
15
15
  * how does participation looks like in attestations.
16
16
  */
17
17
  const MAX_RETAINED_ATTESTATIONS_PER_GROUP = 4;
18
+ /**
19
+ * This is the same to MAX_RETAINED_ATTESTATIONS_PER_GROUP but for electra.
20
+ * As monitored in hoodi, max attestations per group could be up to > 10. But since electra we can
21
+ * consolidate attestations across committees, so we can just pick up to 8 attestations per group.
22
+ * Also the MatchingDataAttestationGroup.getAttestationsForBlock() is improved not to have to scan each
23
+ * committee member for previous slot.
24
+ */
25
+ const MAX_RETAINED_ATTESTATIONS_PER_GROUP_ELECTRA = 8;
18
26
  /**
19
27
  * Pre-electra, each slot has 64 committees, and each block has 128 attestations max so in average
20
28
  * we get 2 attestation per groups.
@@ -24,20 +32,30 @@ const MAX_RETAINED_ATTESTATIONS_PER_GROUP = 4;
24
32
  */
25
33
  const MAX_ATTESTATIONS_PER_GROUP = 3;
26
34
  /**
27
- * For electra, each block has up to 8 aggregated attestations, assuming there are 3 for the "best"
28
- * attestation data, there are still 5 for other attestation data so this constant is still good.
29
- * We should separate to 2 constant based on conditions of different networks
35
+ * For electra, there is on chain aggregation of attestations across committees, so we can just pick up to 8
36
+ * attestations per group, sort by scores to get first 8.
37
+ * The new algorithm helps not to include useless attestations so we usually cannot get up to 8.
38
+ * The more consolidations we have per block, the less likely we have to scan all slots in the pool.
39
+ * This is max attestations returned per group, it does not make sense to have this number greater
40
+ * than MAX_RETAINED_ATTESTATIONS_PER_GROUP_ELECTRA or MAX_ATTESTATIONS_ELECTRA.
30
41
  */
31
- const MAX_ATTESTATIONS_PER_GROUP_ELECTRA = 3;
42
+ const MAX_ATTESTATIONS_PER_GROUP_ELECTRA = Math.min(MAX_RETAINED_ATTESTATIONS_PER_GROUP_ELECTRA, MAX_ATTESTATIONS_ELECTRA);
43
+ export var ScannedSlotsTerminationReason;
44
+ (function (ScannedSlotsTerminationReason) {
45
+ ScannedSlotsTerminationReason["MaxConsolidationReached"] = "max_consolidation_reached";
46
+ ScannedSlotsTerminationReason["ScannedAllSlots"] = "scanned_all_slots";
47
+ ScannedSlotsTerminationReason["SlotBeforePreviousEpoch"] = "slot_before_previous_epoch";
48
+ })(ScannedSlotsTerminationReason || (ScannedSlotsTerminationReason = {}));
32
49
  /**
33
50
  * Maintain a pool of aggregated attestations. Attestations can be retrieved for inclusion in a block
34
- * or api. The returned attestations are aggregated to maximise the number of validators that can be
51
+ * or api. The returned attestations are aggregated to maximize the number of validators that can be
35
52
  * included.
36
53
  * Note that we want to remove attestations with attesters that were included in the chain.
37
54
  */
38
55
  export class AggregatedAttestationPool {
39
- constructor(config) {
56
+ constructor(config, metrics = null) {
40
57
  this.config = config;
58
+ this.metrics = metrics;
41
59
  /**
42
60
  * post electra, different committees could have the same AttData and we have to consolidate attestations of the same
43
61
  * data to be included in block, so we should group by data before index
@@ -45,20 +63,7 @@ export class AggregatedAttestationPool {
45
63
  */
46
64
  this.attestationGroupByIndexByDataHexBySlot = new MapDef(() => new Map());
47
65
  this.lowestPermissibleSlot = 0;
48
- }
49
- /** For metrics to track size of the pool */
50
- getAttestationCount() {
51
- let attestationCount = 0;
52
- let attestationDataCount = 0;
53
- for (const attestationGroupByIndexByDataHex of this.attestationGroupByIndexByDataHexBySlot.values()) {
54
- for (const attestationGroupByIndex of attestationGroupByIndexByDataHex.values()) {
55
- attestationDataCount += attestationGroupByIndex.size;
56
- for (const attestationGroup of attestationGroupByIndex.values()) {
57
- attestationCount += attestationGroup.getAttestationCount();
58
- }
59
- }
60
- }
61
- return { attestationCount, attestationDataCount };
66
+ metrics?.opPool.aggregatedAttestationPool.attDataPerSlot.addCollect(() => this.onScrapeMetrics(metrics));
62
67
  }
63
68
  add(attestation, dataRootHex, attestingIndicesCount, committee) {
64
69
  const slot = attestation.data.slot;
@@ -90,7 +95,7 @@ export class AggregatedAttestationPool {
90
95
  assert.notNull(committeeIndex, "Committee index should not be null in aggregated attestation pool");
91
96
  let attestationGroup = attestationGroupByIndex.get(committeeIndex);
92
97
  if (!attestationGroup) {
93
- attestationGroup = new MatchingDataAttestationGroup(committee, attestation.data);
98
+ attestationGroup = new MatchingDataAttestationGroup(this.config, committee, attestation.data);
94
99
  attestationGroupByIndex.set(committeeIndex, attestationGroup);
95
100
  }
96
101
  return attestationGroup.add({
@@ -146,16 +151,16 @@ export class AggregatedAttestationPool {
146
151
  (ForkSeq[fork] >= ForkSeq.deneb || stateSlot <= slot + SLOTS_PER_EPOCH))) {
147
152
  continue; // Invalid attestations
148
153
  }
149
- const slotDelta = stateSlot - slot;
154
+ const inclusionDistance = stateSlot - slot;
150
155
  for (const attestationGroupByIndex of attestationGroupByIndexByDataHash.values()) {
151
156
  for (const [committeeIndex, attestationGroup] of attestationGroupByIndex.entries()) {
152
- const notSeenAttestingIndices = notSeenValidatorsFn(epoch, slot, committeeIndex);
153
- if (notSeenAttestingIndices === null || notSeenAttestingIndices.size === 0) {
157
+ const notSeenCommitteeMembers = notSeenValidatorsFn(epoch, slot, committeeIndex);
158
+ if (notSeenCommitteeMembers === null || notSeenCommitteeMembers.size === 0) {
154
159
  continue;
155
160
  }
156
161
  if (slotCount > 2 &&
157
162
  attestationsByScore.length >= MAX_ATTESTATIONS &&
158
- notSeenAttestingIndices.size / slotDelta < minScore) {
163
+ notSeenCommitteeMembers.size / inclusionDistance < minScore) {
159
164
  // after 2 slots, there are a good chance that we have 2 * MAX_ATTESTATIONS attestations and break the for loop early
160
165
  // if not, we may have to scan all slots in the pool
161
166
  // if we have enough attestations and the max possible score is lower than scores of `attestationsByScore`, we should skip
@@ -172,8 +177,9 @@ export class AggregatedAttestationPool {
172
177
  // These properties should not change after being validate in gossip
173
178
  // IF they have to be validated, do it only with one attestation per group since same data
174
179
  // The committeeCountPerSlot can be precomputed once per slot
175
- for (const { attestation, notSeenAttesterCount } of attestationGroup.getAttestationsForBlock(fork, notSeenAttestingIndices)) {
176
- const score = notSeenAttesterCount / slotDelta;
180
+ const getAttestationsResult = attestationGroup.getAttestationsForBlock(fork, state.epochCtx.effectiveBalanceIncrements, notSeenCommitteeMembers, MAX_ATTESTATIONS_PER_GROUP);
181
+ for (const { attestation, newSeenEffectiveBalance } of getAttestationsResult.result) {
182
+ const score = newSeenEffectiveBalance / inclusionDistance;
177
183
  if (score < minScore) {
178
184
  minScore = score;
179
185
  }
@@ -212,49 +218,56 @@ export class AggregatedAttestationPool {
212
218
  const slots = Array.from(this.attestationGroupByIndexByDataHexBySlot.keys()).sort((a, b) => b - a);
213
219
  // Track score of each `AttestationsConsolidation`
214
220
  const consolidations = new Map();
215
- let minScore = Number.MAX_SAFE_INTEGER;
216
- let slotCount = 0;
221
+ let scannedSlots = 0;
222
+ let stopReason = null;
217
223
  slot: for (const slot of slots) {
218
- slotCount++;
219
224
  const attestationGroupByIndexByDataHash = this.attestationGroupByIndexByDataHexBySlot.get(slot);
220
225
  // should not happen
221
226
  if (!attestationGroupByIndexByDataHash) {
222
227
  throw Error(`No aggregated attestation pool for slot=${slot}`);
223
228
  }
224
229
  const epoch = computeEpochAtSlot(slot);
230
+ if (epoch < statePrevEpoch) {
231
+ // we process slot in desc order, this means next slot is not eligible, we should stop
232
+ stopReason = ScannedSlotsTerminationReason.SlotBeforePreviousEpoch;
233
+ break;
234
+ }
225
235
  // validateAttestation condition: Attestation target epoch not in previous or current epoch
226
236
  if (!(epoch === stateEpoch || epoch === statePrevEpoch)) {
227
237
  continue; // Invalid attestations
228
238
  }
229
239
  // validateAttestation condition: Attestation slot not within inclusion window
230
240
  if (!(slot + MIN_ATTESTATION_INCLUSION_DELAY <= stateSlot)) {
241
+ // this should not happen as slot is decreased so no need to track in metric
231
242
  continue; // Invalid attestations
232
243
  }
233
- const slotDelta = stateSlot - slot;
234
- // CommitteeIndex 0 1 2 ... Consolidation
244
+ const inclusionDistance = stateSlot - slot;
245
+ let returnedAttestationsPerSlot = 0;
246
+ let totalAttestationsPerSlot = 0;
247
+ // CommitteeIndex 0 1 2 ... Consolidation (sameAttDataCons)
235
248
  // Attestations att00 --- att10 --- att20 --- 0 (att 00 10 20)
236
249
  // att01 --- - --- att21 --- 1 (att 01 __ 21)
237
250
  // - --- - --- att22 --- 2 (att __ __ 22)
238
251
  for (const attestationGroupByIndex of attestationGroupByIndexByDataHash.values()) {
239
252
  // sameAttDataCons could be up to MAX_ATTESTATIONS_PER_GROUP_ELECTRA
240
253
  const sameAttDataCons = [];
254
+ const allAttestationGroups = Array.from(attestationGroupByIndex.values());
255
+ if (allAttestationGroups.length === 0) {
256
+ this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.emptyAttestationData.inc();
257
+ continue;
258
+ }
259
+ if (!validateAttestationDataFn(allAttestationGroups[0].data)) {
260
+ this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.invalidAttestationData.inc();
261
+ continue;
262
+ }
241
263
  for (const [committeeIndex, attestationGroup] of attestationGroupByIndex.entries()) {
242
- const notSeenAttestingIndices = notSeenValidatorsFn(epoch, slot, committeeIndex);
243
- if (notSeenAttestingIndices === null || notSeenAttestingIndices.size === 0) {
244
- continue;
245
- }
246
- if (slotCount > 2 &&
247
- consolidations.size >= MAX_ATTESTATIONS_ELECTRA &&
248
- notSeenAttestingIndices.size / slotDelta < minScore) {
249
- // after 2 slots, there are a good chance that we have 2 * MAX_ATTESTATIONS_ELECTRA attestations and break the for loop early
250
- // if not, we may have to scan all slots in the pool
251
- // if we have enough attestations and the max possible score is lower than scores of `attestationsByScore`, we should skip
252
- // otherwise it takes time to check attestation, add it and remove it later after the sort by score
253
- continue;
254
- }
255
- if (!validateAttestationDataFn(attestationGroup.data)) {
264
+ const notSeenCommitteeMembers = notSeenValidatorsFn(epoch, slot, committeeIndex);
265
+ if (notSeenCommitteeMembers === null || notSeenCommitteeMembers.size === 0) {
266
+ this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.seenCommittees.inc();
256
267
  continue;
257
268
  }
269
+ // cannot apply this optimization like pre-electra because consolidation needs to be done across committees:
270
+ // "after 2 slots, there are a good chance that we have 2 * MAX_ATTESTATIONS_ELECTRA attestations and break the for loop early"
258
271
  // TODO: Is it necessary to validateAttestation for:
259
272
  // - Attestation committee index not within current committee count
260
273
  // - Attestation aggregation bits length does not match committee length
@@ -262,39 +275,73 @@ export class AggregatedAttestationPool {
262
275
  // These properties should not change after being validate in gossip
263
276
  // IF they have to be validated, do it only with one attestation per group since same data
264
277
  // The committeeCountPerSlot can be precomputed once per slot
265
- for (const [i, attestationNonParticipation] of attestationGroup
266
- .getAttestationsForBlock(fork, notSeenAttestingIndices)
267
- .entries()) {
278
+ const getAttestationGroupResult = attestationGroup.getAttestationsForBlock(fork, state.epochCtx.effectiveBalanceIncrements, notSeenCommitteeMembers, MAX_ATTESTATIONS_PER_GROUP_ELECTRA);
279
+ const attestationsSameGroup = getAttestationGroupResult.result;
280
+ returnedAttestationsPerSlot += attestationsSameGroup.length;
281
+ totalAttestationsPerSlot += getAttestationGroupResult.totalAttestations;
282
+ for (const [i, attestationNonParticipation] of attestationsSameGroup.entries()) {
283
+ // sameAttDataCons shares the same index for different committees so we use index `i` here
268
284
  if (sameAttDataCons[i] === undefined) {
269
285
  sameAttDataCons[i] = {
270
286
  byCommittee: new Map(),
271
287
  attData: attestationNonParticipation.attestation.data,
272
- totalNotSeenCount: 0,
288
+ totalNewSeenEffectiveBalance: 0,
289
+ newSeenAttesters: 0,
290
+ notSeenAttesters: 0,
291
+ totalAttesters: 0,
273
292
  };
274
293
  }
275
- sameAttDataCons[i].byCommittee.set(committeeIndex, attestationNonParticipation);
276
- sameAttDataCons[i].totalNotSeenCount += attestationNonParticipation.notSeenAttesterCount;
277
- }
278
- for (const consolidation of sameAttDataCons) {
279
- const score = consolidation.totalNotSeenCount / slotDelta;
280
- if (score < minScore) {
281
- minScore = score;
282
- }
283
- consolidations.set(consolidation, score);
284
- // Stop accumulating attestations there are enough that may have good scoring
285
- if (consolidations.size >= MAX_ATTESTATIONS_ELECTRA * 2) {
286
- break slot;
294
+ const sameAttDataCon = sameAttDataCons[i];
295
+ // committeeIndex was from a map so it should be unique, but just in case
296
+ if (!sameAttDataCon.byCommittee.has(committeeIndex)) {
297
+ sameAttDataCon.byCommittee.set(committeeIndex, attestationNonParticipation);
298
+ sameAttDataCon.totalNewSeenEffectiveBalance += attestationNonParticipation.newSeenEffectiveBalance;
299
+ sameAttDataCon.newSeenAttesters += attestationNonParticipation.newSeenAttesters;
300
+ sameAttDataCon.notSeenAttesters += attestationNonParticipation.notSeenCommitteeMembers.size;
301
+ sameAttDataCon.totalAttesters += attestationGroup.committee.length;
287
302
  }
288
303
  }
304
+ } // all committees are processed
305
+ this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.returnedAttestations.set({ inclusionDistance }, returnedAttestationsPerSlot);
306
+ this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.scannedAttestations.set({ inclusionDistance }, totalAttestationsPerSlot);
307
+ // after all committees are processed, we have a list of sameAttDataCons
308
+ for (const consolidation of sameAttDataCons) {
309
+ const score = consolidation.totalNewSeenEffectiveBalance / inclusionDistance;
310
+ consolidations.set(consolidation, score);
311
+ // Stop accumulating attestations there are enough that may have good scoring
312
+ if (consolidations.size >= MAX_ATTESTATIONS_ELECTRA * 2) {
313
+ stopReason = ScannedSlotsTerminationReason.MaxConsolidationReached;
314
+ break slot;
315
+ }
289
316
  }
290
317
  }
318
+ // finished processing a slot
319
+ scannedSlots++;
291
320
  }
321
+ this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.totalConsolidations.set(consolidations.size);
292
322
  const sortedConsolidationsByScore = Array.from(consolidations.entries())
293
323
  .sort((a, b) => b[1] - a[1])
294
324
  .map(([consolidation, _]) => consolidation)
295
325
  .slice(0, MAX_ATTESTATIONS_ELECTRA);
296
326
  // on chain aggregation is expensive, only do it after all
297
- return sortedConsolidationsByScore.map(aggregateConsolidation);
327
+ const packedAttestationsMetrics = this.metrics?.opPool.aggregatedAttestationPool.packedAttestations;
328
+ const packedAttestations = new Array(sortedConsolidationsByScore.length);
329
+ for (const [i, consolidation] of sortedConsolidationsByScore.entries()) {
330
+ packedAttestations[i] = aggregateConsolidation(consolidation);
331
+ // record metrics of packed attestations
332
+ packedAttestationsMetrics?.committeeCount.set({ index: i }, consolidation.byCommittee.size);
333
+ packedAttestationsMetrics?.totalAttesters.set({ index: i }, consolidation.totalAttesters);
334
+ packedAttestationsMetrics?.nonParticipation.set({ index: i }, consolidation.notSeenAttesters);
335
+ packedAttestationsMetrics?.inclusionDistance.set({ index: i }, stateSlot - packedAttestations[i].data.slot);
336
+ packedAttestationsMetrics?.newSeenAttesters.set({ index: i }, consolidation.newSeenAttesters);
337
+ packedAttestationsMetrics?.totalEffectiveBalance.set({ index: i }, consolidation.totalNewSeenEffectiveBalance);
338
+ }
339
+ if (stopReason === null) {
340
+ stopReason = ScannedSlotsTerminationReason.ScannedAllSlots;
341
+ }
342
+ packedAttestationsMetrics?.scannedSlots.set({ reason: stopReason }, scannedSlots);
343
+ packedAttestationsMetrics?.poolSlots.set(slots.length);
344
+ return packedAttestations;
298
345
  }
299
346
  /**
300
347
  * Get all attestations optionally filtered by `attestation.data.slot`
@@ -323,6 +370,49 @@ export class AggregatedAttestationPool {
323
370
  }
324
371
  return attestations;
325
372
  }
373
+ onScrapeMetrics(metrics) {
374
+ const poolMetrics = metrics.opPool.aggregatedAttestationPool;
375
+ const allSlots = Array.from(this.attestationGroupByIndexByDataHexBySlot.keys());
376
+ // last item is current slot, we want the previous one, if available.
377
+ const previousSlot = allSlots.length > 1 ? (allSlots.at(-2) ?? null) : null;
378
+ let attestationCount = 0;
379
+ let attestationDataCount = 0;
380
+ // always record the previous slot because the current slot may not be finished yet, we may receive more attestations
381
+ if (previousSlot !== null) {
382
+ const groupByIndexByDataHex = this.attestationGroupByIndexByDataHexBySlot.get(previousSlot);
383
+ if (groupByIndexByDataHex != null) {
384
+ poolMetrics.attDataPerSlot.set(groupByIndexByDataHex.size);
385
+ let maxAttestations = 0;
386
+ let committeeCount = 0;
387
+ for (const groupByIndex of groupByIndexByDataHex.values()) {
388
+ attestationDataCount += groupByIndex.size;
389
+ for (const group of groupByIndex.values()) {
390
+ const attestationCountInGroup = group.getAttestationCount();
391
+ maxAttestations = Math.max(maxAttestations, attestationCountInGroup);
392
+ poolMetrics.attestationsPerCommittee.observe(attestationCountInGroup);
393
+ committeeCount += 1;
394
+ attestationCount += attestationCountInGroup;
395
+ }
396
+ }
397
+ poolMetrics.maxAttestationsPerCommittee.set(maxAttestations);
398
+ poolMetrics.committeesPerSlot.set(committeeCount);
399
+ }
400
+ }
401
+ for (const [slot, attestationGroupByIndexByDataHex] of this.attestationGroupByIndexByDataHexBySlot) {
402
+ // We have already updated attestationDataCount and attestationCount when looping over `previousSlot`
403
+ if (slot === previousSlot) {
404
+ continue;
405
+ }
406
+ for (const attestationGroupByIndex of attestationGroupByIndexByDataHex.values()) {
407
+ attestationDataCount += attestationGroupByIndex.size;
408
+ for (const attestationGroup of attestationGroupByIndex.values()) {
409
+ attestationCount += attestationGroup.getAttestationCount();
410
+ }
411
+ }
412
+ }
413
+ poolMetrics.size.set(attestationCount);
414
+ poolMetrics.uniqueData.set(attestationDataCount);
415
+ }
326
416
  }
327
417
  /**
328
418
  * Maintain a pool of AggregatedAttestation which all share the same AttestationData.
@@ -331,9 +421,8 @@ export class AggregatedAttestationPool {
331
421
  * Use committee instead of aggregationBits to improve performance.
332
422
  */
333
423
  export class MatchingDataAttestationGroup {
334
- constructor(
335
- // TODO: no need committee here
336
- committee, data) {
424
+ constructor(config, committee, data) {
425
+ this.config = config;
337
426
  this.committee = committee;
338
427
  this.data = data;
339
428
  this.attestations = [];
@@ -373,42 +462,86 @@ export class MatchingDataAttestationGroup {
373
462
  this.attestations.splice(index, 1);
374
463
  }
375
464
  this.attestations.push(attestation);
465
+ const maxRetained = isForkPostElectra(this.config.getForkName(this.data.slot))
466
+ ? MAX_RETAINED_ATTESTATIONS_PER_GROUP_ELECTRA
467
+ : MAX_RETAINED_ATTESTATIONS_PER_GROUP;
376
468
  // Remove the attestations with less participation
377
- if (this.attestations.length > MAX_RETAINED_ATTESTATIONS_PER_GROUP) {
469
+ if (this.attestations.length > maxRetained) {
470
+ // ideally we should sort by effective balance but there is no state/effectiveBalance here
471
+ // it's rare to see > 8 attestations per group in electra anyway
378
472
  this.attestations.sort((a, b) => b.trueBitsCount - a.trueBitsCount);
379
- this.attestations.splice(MAX_RETAINED_ATTESTATIONS_PER_GROUP, this.attestations.length - MAX_RETAINED_ATTESTATIONS_PER_GROUP);
473
+ this.attestations.splice(maxRetained, this.attestations.length - maxRetained);
380
474
  }
381
475
  return InsertOutcome.NewData;
382
476
  }
383
477
  /**
384
478
  * Get AttestationNonParticipant for this groups of same attestation data.
385
- * @param notSeenAttestingIndices not seen attestting indices, i.e. indices in the same committee
479
+ * @param notSeenCommitteeMembers not seen committee members, i.e. indices in the same committee (starting from 0 till (committee.size - 1))
386
480
  * @returns an array of AttestationNonParticipant
387
481
  */
388
- getAttestationsForBlock(fork, notSeenAttestingIndices) {
482
+ getAttestationsForBlock(fork, effectiveBalanceIncrements, notSeenCommitteeMembers, maxAttestation) {
389
483
  const attestations = [];
484
+ const excluded = new Set();
485
+ for (let i = 0; i < maxAttestation; i++) {
486
+ const mostValuableAttestation = this.getMostValuableAttestation(fork, effectiveBalanceIncrements, notSeenCommitteeMembers, excluded);
487
+ if (mostValuableAttestation === null) {
488
+ // stop looking for attestation because all attesters are seen or no attestation has missing attesters
489
+ break;
490
+ }
491
+ attestations.push(mostValuableAttestation);
492
+ excluded.add(mostValuableAttestation.attestation);
493
+ // this will narrow down the notSeenCommitteeMembers for the next iteration
494
+ // so usually it will not take much time, however it could take more time during
495
+ // non-finality of the network when there is low participation, but in this case
496
+ // we pre-aggregate aggregated attestations and bound the total attestations per group
497
+ notSeenCommitteeMembers = mostValuableAttestation.notSeenCommitteeMembers;
498
+ }
499
+ return { result: attestations, totalAttestations: this.attestations.length };
500
+ }
501
+ /**
502
+ * Select the attestation with the highest total effective balance of not seen validators.
503
+ */
504
+ getMostValuableAttestation(fork, effectiveBalanceIncrements, notSeenCommitteeMembers, excluded) {
505
+ if (notSeenCommitteeMembers.size === 0) {
506
+ // no more attesters to consider
507
+ return null;
508
+ }
390
509
  const isPostElectra = isForkPostElectra(fork);
510
+ let maxNewSeenEffectiveBalance = 0;
511
+ let mostValuableAttestation = null;
391
512
  for (const { attestation } of this.attestations) {
392
513
  if ((isPostElectra && !isElectraAttestation(attestation)) ||
393
514
  (!isPostElectra && isElectraAttestation(attestation))) {
394
515
  continue;
395
516
  }
396
- let notSeenAttesterCount = 0;
517
+ if (excluded.has(attestation)) {
518
+ continue;
519
+ }
520
+ const notSeen = new Set();
521
+ // we prioritize total effective balance over attester count
522
+ let newSeenEffectiveBalance = 0;
523
+ let newSeenAttesters = 0;
397
524
  const { aggregationBits } = attestation;
398
- for (const notSeenIndex of notSeenAttestingIndices) {
525
+ for (const notSeenIndex of notSeenCommitteeMembers) {
399
526
  if (aggregationBits.get(notSeenIndex)) {
400
- notSeenAttesterCount++;
527
+ newSeenEffectiveBalance += effectiveBalanceIncrements[this.committee[notSeenIndex]];
528
+ newSeenAttesters++;
529
+ }
530
+ else {
531
+ notSeen.add(notSeenIndex);
401
532
  }
402
533
  }
403
- if (notSeenAttesterCount > 0) {
404
- attestations.push({ attestation, notSeenAttesterCount });
534
+ if (newSeenEffectiveBalance > maxNewSeenEffectiveBalance) {
535
+ maxNewSeenEffectiveBalance = newSeenEffectiveBalance;
536
+ mostValuableAttestation = {
537
+ attestation,
538
+ newSeenEffectiveBalance,
539
+ newSeenAttesters,
540
+ notSeenCommitteeMembers: notSeen,
541
+ };
405
542
  }
406
543
  }
407
- const maxAttestation = isPostElectra ? MAX_ATTESTATIONS_PER_GROUP_ELECTRA : MAX_ATTESTATIONS_PER_GROUP;
408
- if (attestations.length <= maxAttestation) {
409
- return attestations;
410
- }
411
- return attestations.sort((a, b) => b.notSeenAttesterCount - a.notSeenAttesterCount).slice(0, maxAttestation);
544
+ return mostValuableAttestation;
412
545
  }
413
546
  /** Get attestations for API. */
414
547
  getAttestations() {
@@ -454,13 +587,14 @@ export function aggregateConsolidation({ byCommittee, attData }) {
454
587
  * has already attested or not.
455
588
  */
456
589
  export function getNotSeenValidatorsFn(state) {
457
- if (state.config.getForkName(state.slot) === ForkName.phase0) {
590
+ const stateSlot = state.slot;
591
+ if (state.config.getForkName(stateSlot) === ForkName.phase0) {
458
592
  // Get attestations to be included in a phase0 block.
459
593
  // As we are close to altair, this is not really important, it's mainly for e2e.
460
594
  // The performance is not great due to the different BeaconState data structure to altair.
461
595
  // check for phase0 block already
462
596
  const phase0State = state;
463
- const stateEpoch = computeEpochAtSlot(state.slot);
597
+ const stateEpoch = computeEpochAtSlot(stateSlot);
464
598
  const previousEpochParticipants = extractParticipationPhase0(phase0State.previousEpochAttestations.getAllReadonly(), state);
465
599
  const currentEpochParticipants = extractParticipationPhase0(phase0State.currentEpochAttestations.getAllReadonly(), state);
466
600
  return (epoch, slot, committeeIndex) => {
@@ -469,13 +603,13 @@ export function getNotSeenValidatorsFn(state) {
469
603
  return null;
470
604
  }
471
605
  const committee = state.epochCtx.getBeaconCommittee(slot, committeeIndex);
472
- const notSeenAttestingIndices = new Set();
606
+ const notSeenCommitteeMembers = new Set();
473
607
  for (const [i, validatorIndex] of committee.entries()) {
474
608
  if (!participants.has(validatorIndex)) {
475
- notSeenAttestingIndices.add(i);
609
+ notSeenCommitteeMembers.add(i);
476
610
  }
477
611
  }
478
- return notSeenAttestingIndices.size === 0 ? null : notSeenAttestingIndices;
612
+ return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers;
479
613
  };
480
614
  }
481
615
  // altair and future forks
@@ -486,7 +620,7 @@ export function getNotSeenValidatorsFn(state) {
486
620
  const altairState = state;
487
621
  const previousParticipation = altairState.previousEpochParticipation.getAll();
488
622
  const currentParticipation = altairState.currentEpochParticipation.getAll();
489
- const stateEpoch = computeEpochAtSlot(state.slot);
623
+ const stateEpoch = computeEpochAtSlot(stateSlot);
490
624
  // this function could be called multiple times with same slot + committeeIndex
491
625
  const cachedNotSeenValidators = new Map();
492
626
  return (epoch, slot, committeeIndex) => {
@@ -495,22 +629,23 @@ export function getNotSeenValidatorsFn(state) {
495
629
  return null;
496
630
  }
497
631
  const cacheKey = slot + "_" + committeeIndex;
498
- let notSeenAttestingIndices = cachedNotSeenValidators.get(cacheKey);
499
- if (notSeenAttestingIndices != null) {
632
+ let notSeenCommitteeMembers = cachedNotSeenValidators.get(cacheKey);
633
+ if (notSeenCommitteeMembers != null) {
500
634
  // if all validators are seen then return null, we don't need to check for any attestations of same committee again
501
- return notSeenAttestingIndices.size === 0 ? null : notSeenAttestingIndices;
635
+ return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers;
502
636
  }
503
637
  const committee = state.epochCtx.getBeaconCommittee(slot, committeeIndex);
504
- notSeenAttestingIndices = new Set();
638
+ notSeenCommitteeMembers = new Set();
505
639
  for (const [i, validatorIndex] of committee.entries()) {
506
640
  // no need to check flagIsTimelySource as if validator is not seen, it's participation status is 0
507
- if (participationStatus[validatorIndex] === 0) {
508
- notSeenAttestingIndices.add(i);
641
+ // attestations for the previous slot are not included in the state, so we don't need to check for them
642
+ if (slot === stateSlot - 1 || participationStatus[validatorIndex] === 0) {
643
+ notSeenCommitteeMembers.add(i);
509
644
  }
510
645
  }
511
- cachedNotSeenValidators.set(cacheKey, notSeenAttestingIndices);
646
+ cachedNotSeenValidators.set(cacheKey, notSeenCommitteeMembers);
512
647
  // if all validators are seen then return null, we don't need to check for any attestations of same committee again
513
- return notSeenAttestingIndices.size === 0 ? null : notSeenAttestingIndices;
648
+ return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers;
514
649
  };
515
650
  }
516
651
  export function extractParticipationPhase0(attestations, state) {
@@ -556,8 +691,9 @@ export function getValidateAttestationDataFn(forkChoice, state) {
556
691
  else {
557
692
  return false;
558
693
  }
559
- if (!ssz.phase0.Checkpoint.equals(attData.source, justifiedCheckpoint))
694
+ if (!ssz.phase0.Checkpoint.equals(attData.source, justifiedCheckpoint)) {
560
695
  return false;
696
+ }
561
697
  // Shuffling can't have changed if we're in the first few epochs
562
698
  // Also we can't look back 2 epochs if target epoch is 1 or less
563
699
  if (stateEpoch < 2 || targetEpoch < 2) {
@@ -623,7 +759,9 @@ function isValidShuffling(forkChoice, state, blockRootHex, targetEpoch) {
623
759
  }
624
760
  let attestationDependentRoot;
625
761
  try {
626
- attestationDependentRoot = forkChoice.getDependentRoot(beaconBlock, EpochDifference.previous);
762
+ // should not use forkChoice.getDependentRoot directly, see https://github.com/ChainSafe/lodestar/issues/7651
763
+ // attestationDependentRoot = forkChoice.getDependentRoot(beaconBlock, EpochDifference.previous);
764
+ attestationDependentRoot = getShufflingDependentRoot(forkChoice, targetEpoch, computeEpochAtSlot(beaconBlock.slot), beaconBlock);
627
765
  }
628
766
  catch (_) {
629
767
  // getDependent root may throw error if the dependent root of attestation data is prior to finalized slot