@lodestar/beacon-node 1.29.0-dev.ae2b105931 → 1.29.0-dev.b429528601

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 (112) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +3 -3
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.js +2 -2
  5. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  6. package/lib/api/impl/beacon/state/index.js +12 -0
  7. package/lib/api/impl/beacon/state/index.js.map +1 -1
  8. package/lib/api/impl/validator/index.js +8 -7
  9. package/lib/api/impl/validator/index.js.map +1 -1
  10. package/lib/chain/archiveStore/historicalState/getHistoricalState.js +1 -1
  11. package/lib/chain/archiveStore/historicalState/getHistoricalState.js.map +1 -1
  12. package/lib/chain/archiveStore/historicalState/worker.js +12 -1
  13. package/lib/chain/archiveStore/historicalState/worker.js.map +1 -1
  14. package/lib/chain/blocks/importBlock.js +3 -3
  15. package/lib/chain/blocks/importBlock.js.map +1 -1
  16. package/lib/chain/blocks/verifyBlock.js +1 -1
  17. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  18. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.d.ts +2 -1
  19. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js +2 -2
  20. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js.map +1 -1
  21. package/lib/chain/chain.d.ts +4 -1
  22. package/lib/chain/chain.js +15 -10
  23. package/lib/chain/chain.js.map +1 -1
  24. package/lib/chain/interface.d.ts +2 -0
  25. package/lib/chain/interface.js.map +1 -1
  26. package/lib/chain/opPools/aggregatedAttestationPool.d.ts +32 -14
  27. package/lib/chain/opPools/aggregatedAttestationPool.js +245 -102
  28. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  29. package/lib/chain/opPools/attestationPool.d.ts +4 -2
  30. package/lib/chain/opPools/attestationPool.js +4 -3
  31. package/lib/chain/opPools/attestationPool.js.map +1 -1
  32. package/lib/chain/options.d.ts +2 -1
  33. package/lib/chain/options.js.map +1 -1
  34. package/lib/chain/produceBlock/computeNewStateRoot.js +1 -1
  35. package/lib/chain/produceBlock/computeNewStateRoot.js.map +1 -1
  36. package/lib/chain/regen/queued.d.ts +1 -0
  37. package/lib/chain/regen/queued.js +1 -0
  38. package/lib/chain/regen/queued.js.map +1 -1
  39. package/lib/chain/regen/regen.d.ts +3 -0
  40. package/lib/chain/regen/regen.js +3 -3
  41. package/lib/chain/regen/regen.js.map +1 -1
  42. package/lib/chain/validation/attestation.js +6 -5
  43. package/lib/chain/validation/attestation.js.map +1 -1
  44. package/lib/{metrics → chain}/validatorMonitor.d.ts +3 -2
  45. package/lib/{metrics → chain}/validatorMonitor.js +55 -52
  46. package/lib/chain/validatorMonitor.js.map +1 -0
  47. package/lib/metrics/metrics/beacon.js +1 -1
  48. package/lib/metrics/metrics/beacon.js.map +1 -1
  49. package/lib/metrics/metrics/lodestar.d.ts +64 -13
  50. package/lib/metrics/metrics/lodestar.js +144 -31
  51. package/lib/metrics/metrics/lodestar.js.map +1 -1
  52. package/lib/metrics/metrics.d.ts +2 -6
  53. package/lib/metrics/metrics.js +2 -12
  54. package/lib/metrics/metrics.js.map +1 -1
  55. package/lib/metrics/options.d.ts +2 -3
  56. package/lib/metrics/options.js.map +1 -1
  57. package/lib/network/core/metrics.d.ts +8 -1
  58. package/lib/network/core/metrics.js +10 -0
  59. package/lib/network/core/metrics.js.map +1 -1
  60. package/lib/network/core/networkCore.d.ts +3 -3
  61. package/lib/network/core/networkCore.js +9 -4
  62. package/lib/network/core/networkCore.js.map +1 -1
  63. package/lib/network/core/networkCoreWorker.js +5 -3
  64. package/lib/network/core/networkCoreWorker.js.map +1 -1
  65. package/lib/network/core/networkCoreWorkerHandler.d.ts +2 -2
  66. package/lib/network/core/networkCoreWorkerHandler.js +3 -3
  67. package/lib/network/core/networkCoreWorkerHandler.js.map +1 -1
  68. package/lib/network/core/types.d.ts +1 -1
  69. package/lib/network/discv5/index.d.ts +2 -3
  70. package/lib/network/discv5/index.js +4 -5
  71. package/lib/network/discv5/index.js.map +1 -1
  72. package/lib/network/discv5/types.d.ts +1 -1
  73. package/lib/network/discv5/worker.js +7 -6
  74. package/lib/network/discv5/worker.js.map +1 -1
  75. package/lib/network/gossip/gossipsub.js +4 -0
  76. package/lib/network/gossip/gossipsub.js.map +1 -1
  77. package/lib/network/interface.d.ts +3 -2
  78. package/lib/network/libp2p/error.d.ts +51 -0
  79. package/lib/network/libp2p/error.js +56 -0
  80. package/lib/network/libp2p/error.js.map +1 -0
  81. package/lib/network/libp2p/index.d.ts +2 -2
  82. package/lib/network/libp2p/index.js +15 -9
  83. package/lib/network/libp2p/index.js.map +1 -1
  84. package/lib/network/network.d.ts +4 -4
  85. package/lib/network/network.js +7 -5
  86. package/lib/network/network.js.map +1 -1
  87. package/lib/network/peers/datastore.d.ts +2 -1
  88. package/lib/network/peers/datastore.js +1 -1
  89. package/lib/network/peers/datastore.js.map +1 -1
  90. package/lib/network/peers/discover.d.ts +7 -0
  91. package/lib/network/peers/discover.js +16 -5
  92. package/lib/network/peers/discover.js.map +1 -1
  93. package/lib/network/peers/peerManager.d.ts +2 -1
  94. package/lib/network/peers/peerManager.js +1 -1
  95. package/lib/network/peers/peerManager.js.map +1 -1
  96. package/lib/network/peers/utils/getConnectedPeerIds.js +2 -2
  97. package/lib/network/peers/utils/getConnectedPeerIds.js.map +1 -1
  98. package/lib/network/processor/gossipHandlers.js +8 -7
  99. package/lib/network/processor/gossipHandlers.js.map +1 -1
  100. package/lib/network/util.d.ts +4 -1
  101. package/lib/network/util.js +2 -2
  102. package/lib/network/util.js.map +1 -1
  103. package/lib/node/nodejs.d.ts +7 -4
  104. package/lib/node/nodejs.js +11 -4
  105. package/lib/node/nodejs.js.map +1 -1
  106. package/lib/node/options.d.ts +2 -0
  107. package/lib/node/options.js +2 -0
  108. package/lib/node/options.js.map +1 -1
  109. package/lib/util/peerId.js +1 -1
  110. package/lib/util/peerId.js.map +1 -1
  111. package/package.json +31 -32
  112. package/lib/metrics/validatorMonitor.js.map +0 -1
@@ -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
- import { ForkName, ForkSeq, MAX_ATTESTATIONS, MAX_ATTESTATIONS_ELECTRA, MAX_COMMITTEES_PER_SLOT, MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, isForkPostElectra, } from "@lodestar/params";
5
- import { computeEpochAtSlot, computeStartSlotAtEpoch, getBlockRootAtSlot, } from "@lodestar/state-transition";
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";
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({
@@ -100,9 +105,14 @@ export class AggregatedAttestationPool {
100
105
  }
101
106
  /** Remove attestations which are too old to be included in a block. */
102
107
  prune(clockSlot) {
103
- // Only retain SLOTS_PER_EPOCH slots
104
- pruneBySlot(this.attestationGroupByIndexByDataHexBySlot, clockSlot, SLOTS_PER_EPOCH);
105
- this.lowestPermissibleSlot = Math.max(clockSlot - SLOTS_PER_EPOCH, 0);
108
+ const fork = this.config.getForkName(clockSlot);
109
+ const slotsToRetain = isForkPostDeneb(fork)
110
+ ? // Post deneb, attestations from current and previous epoch can be included
111
+ computeSlotsSinceEpochStart(clockSlot, computeEpochAtSlot(clockSlot) - 1)
112
+ : // Before deneb, only retain SLOTS_PER_EPOCH slots
113
+ SLOTS_PER_EPOCH;
114
+ pruneBySlot(this.attestationGroupByIndexByDataHexBySlot, clockSlot, slotsToRetain);
115
+ this.lowestPermissibleSlot = Math.max(clockSlot - slotsToRetain, 0);
106
116
  }
107
117
  getAttestationsForBlock(fork, forkChoice, state) {
108
118
  const forkSeq = ForkSeq[fork];
@@ -141,16 +151,16 @@ export class AggregatedAttestationPool {
141
151
  (ForkSeq[fork] >= ForkSeq.deneb || stateSlot <= slot + SLOTS_PER_EPOCH))) {
142
152
  continue; // Invalid attestations
143
153
  }
144
- const slotDelta = stateSlot - slot;
154
+ const inclusionDistance = stateSlot - slot;
145
155
  for (const attestationGroupByIndex of attestationGroupByIndexByDataHash.values()) {
146
156
  for (const [committeeIndex, attestationGroup] of attestationGroupByIndex.entries()) {
147
- const notSeenAttestingIndices = notSeenValidatorsFn(epoch, slot, committeeIndex);
148
- if (notSeenAttestingIndices === null || notSeenAttestingIndices.size === 0) {
157
+ const notSeenCommitteeMembers = notSeenValidatorsFn(epoch, slot, committeeIndex);
158
+ if (notSeenCommitteeMembers === null || notSeenCommitteeMembers.size === 0) {
149
159
  continue;
150
160
  }
151
161
  if (slotCount > 2 &&
152
162
  attestationsByScore.length >= MAX_ATTESTATIONS &&
153
- notSeenAttestingIndices.size / slotDelta < minScore) {
163
+ notSeenCommitteeMembers.size / inclusionDistance < minScore) {
154
164
  // after 2 slots, there are a good chance that we have 2 * MAX_ATTESTATIONS attestations and break the for loop early
155
165
  // if not, we may have to scan all slots in the pool
156
166
  // if we have enough attestations and the max possible score is lower than scores of `attestationsByScore`, we should skip
@@ -167,8 +177,9 @@ export class AggregatedAttestationPool {
167
177
  // These properties should not change after being validate in gossip
168
178
  // IF they have to be validated, do it only with one attestation per group since same data
169
179
  // The committeeCountPerSlot can be precomputed once per slot
170
- for (const { attestation, notSeenAttesterCount } of attestationGroup.getAttestationsForBlock(fork, notSeenAttestingIndices)) {
171
- 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;
172
183
  if (score < minScore) {
173
184
  minScore = score;
174
185
  }
@@ -207,49 +218,56 @@ export class AggregatedAttestationPool {
207
218
  const slots = Array.from(this.attestationGroupByIndexByDataHexBySlot.keys()).sort((a, b) => b - a);
208
219
  // Track score of each `AttestationsConsolidation`
209
220
  const consolidations = new Map();
210
- let minScore = Number.MAX_SAFE_INTEGER;
211
- let slotCount = 0;
221
+ let scannedSlots = 0;
222
+ let stopReason = null;
212
223
  slot: for (const slot of slots) {
213
- slotCount++;
214
224
  const attestationGroupByIndexByDataHash = this.attestationGroupByIndexByDataHexBySlot.get(slot);
215
225
  // should not happen
216
226
  if (!attestationGroupByIndexByDataHash) {
217
227
  throw Error(`No aggregated attestation pool for slot=${slot}`);
218
228
  }
219
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
+ }
220
235
  // validateAttestation condition: Attestation target epoch not in previous or current epoch
221
236
  if (!(epoch === stateEpoch || epoch === statePrevEpoch)) {
222
237
  continue; // Invalid attestations
223
238
  }
224
239
  // validateAttestation condition: Attestation slot not within inclusion window
225
240
  if (!(slot + MIN_ATTESTATION_INCLUSION_DELAY <= stateSlot)) {
241
+ // this should not happen as slot is decreased so no need to track in metric
226
242
  continue; // Invalid attestations
227
243
  }
228
- const slotDelta = stateSlot - slot;
229
- // 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)
230
248
  // Attestations att00 --- att10 --- att20 --- 0 (att 00 10 20)
231
249
  // att01 --- - --- att21 --- 1 (att 01 __ 21)
232
250
  // - --- - --- att22 --- 2 (att __ __ 22)
233
251
  for (const attestationGroupByIndex of attestationGroupByIndexByDataHash.values()) {
234
252
  // sameAttDataCons could be up to MAX_ATTESTATIONS_PER_GROUP_ELECTRA
235
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
+ }
236
263
  for (const [committeeIndex, attestationGroup] of attestationGroupByIndex.entries()) {
237
- const notSeenAttestingIndices = notSeenValidatorsFn(epoch, slot, committeeIndex);
238
- if (notSeenAttestingIndices === null || notSeenAttestingIndices.size === 0) {
239
- continue;
240
- }
241
- if (slotCount > 2 &&
242
- consolidations.size >= MAX_ATTESTATIONS_ELECTRA &&
243
- notSeenAttestingIndices.size / slotDelta < minScore) {
244
- // after 2 slots, there are a good chance that we have 2 * MAX_ATTESTATIONS_ELECTRA attestations and break the for loop early
245
- // if not, we may have to scan all slots in the pool
246
- // if we have enough attestations and the max possible score is lower than scores of `attestationsByScore`, we should skip
247
- // otherwise it takes time to check attestation, add it and remove it later after the sort by score
248
- continue;
249
- }
250
- 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();
251
267
  continue;
252
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"
253
271
  // TODO: Is it necessary to validateAttestation for:
254
272
  // - Attestation committee index not within current committee count
255
273
  // - Attestation aggregation bits length does not match committee length
@@ -257,39 +275,73 @@ export class AggregatedAttestationPool {
257
275
  // These properties should not change after being validate in gossip
258
276
  // IF they have to be validated, do it only with one attestation per group since same data
259
277
  // The committeeCountPerSlot can be precomputed once per slot
260
- for (const [i, attestationNonParticipation] of attestationGroup
261
- .getAttestationsForBlock(fork, notSeenAttestingIndices)
262
- .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
263
284
  if (sameAttDataCons[i] === undefined) {
264
285
  sameAttDataCons[i] = {
265
286
  byCommittee: new Map(),
266
287
  attData: attestationNonParticipation.attestation.data,
267
- totalNotSeenCount: 0,
288
+ totalNewSeenEffectiveBalance: 0,
289
+ newSeenAttesters: 0,
290
+ notSeenAttesters: 0,
291
+ totalAttesters: 0,
268
292
  };
269
293
  }
270
- sameAttDataCons[i].byCommittee.set(committeeIndex, attestationNonParticipation);
271
- sameAttDataCons[i].totalNotSeenCount += attestationNonParticipation.notSeenAttesterCount;
272
- }
273
- for (const consolidation of sameAttDataCons) {
274
- const score = consolidation.totalNotSeenCount / slotDelta;
275
- if (score < minScore) {
276
- minScore = score;
277
- }
278
- consolidations.set(consolidation, score);
279
- // Stop accumulating attestations there are enough that may have good scoring
280
- if (consolidations.size >= MAX_ATTESTATIONS_ELECTRA * 2) {
281
- 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;
282
302
  }
283
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
+ }
284
316
  }
285
317
  }
318
+ // finished processing a slot
319
+ scannedSlots++;
286
320
  }
321
+ this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.totalConsolidations.set(consolidations.size);
287
322
  const sortedConsolidationsByScore = Array.from(consolidations.entries())
288
323
  .sort((a, b) => b[1] - a[1])
289
324
  .map(([consolidation, _]) => consolidation)
290
325
  .slice(0, MAX_ATTESTATIONS_ELECTRA);
291
326
  // on chain aggregation is expensive, only do it after all
292
- 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;
293
345
  }
294
346
  /**
295
347
  * Get all attestations optionally filtered by `attestation.data.slot`
@@ -318,6 +370,49 @@ export class AggregatedAttestationPool {
318
370
  }
319
371
  return attestations;
320
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
+ }
321
416
  }
322
417
  /**
323
418
  * Maintain a pool of AggregatedAttestation which all share the same AttestationData.
@@ -326,9 +421,8 @@ export class AggregatedAttestationPool {
326
421
  * Use committee instead of aggregationBits to improve performance.
327
422
  */
328
423
  export class MatchingDataAttestationGroup {
329
- constructor(
330
- // TODO: no need committee here
331
- committee, data) {
424
+ constructor(config, committee, data) {
425
+ this.config = config;
332
426
  this.committee = committee;
333
427
  this.data = data;
334
428
  this.attestations = [];
@@ -368,42 +462,86 @@ export class MatchingDataAttestationGroup {
368
462
  this.attestations.splice(index, 1);
369
463
  }
370
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;
371
468
  // Remove the attestations with less participation
372
- 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
373
472
  this.attestations.sort((a, b) => b.trueBitsCount - a.trueBitsCount);
374
- 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);
375
474
  }
376
475
  return InsertOutcome.NewData;
377
476
  }
378
477
  /**
379
478
  * Get AttestationNonParticipant for this groups of same attestation data.
380
- * @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))
381
480
  * @returns an array of AttestationNonParticipant
382
481
  */
383
- getAttestationsForBlock(fork, notSeenAttestingIndices) {
482
+ getAttestationsForBlock(fork, effectiveBalanceIncrements, notSeenCommitteeMembers, maxAttestation) {
384
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
+ }
385
509
  const isPostElectra = isForkPostElectra(fork);
510
+ let maxNewSeenEffectiveBalance = 0;
511
+ let mostValuableAttestation = null;
386
512
  for (const { attestation } of this.attestations) {
387
513
  if ((isPostElectra && !isElectraAttestation(attestation)) ||
388
514
  (!isPostElectra && isElectraAttestation(attestation))) {
389
515
  continue;
390
516
  }
391
- 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;
392
524
  const { aggregationBits } = attestation;
393
- for (const notSeenIndex of notSeenAttestingIndices) {
525
+ for (const notSeenIndex of notSeenCommitteeMembers) {
394
526
  if (aggregationBits.get(notSeenIndex)) {
395
- notSeenAttesterCount++;
527
+ newSeenEffectiveBalance += effectiveBalanceIncrements[this.committee[notSeenIndex]];
528
+ newSeenAttesters++;
529
+ }
530
+ else {
531
+ notSeen.add(notSeenIndex);
396
532
  }
397
533
  }
398
- if (notSeenAttesterCount > 0) {
399
- attestations.push({ attestation, notSeenAttesterCount });
534
+ if (newSeenEffectiveBalance > maxNewSeenEffectiveBalance) {
535
+ maxNewSeenEffectiveBalance = newSeenEffectiveBalance;
536
+ mostValuableAttestation = {
537
+ attestation,
538
+ newSeenEffectiveBalance,
539
+ newSeenAttesters,
540
+ notSeenCommitteeMembers: notSeen,
541
+ };
400
542
  }
401
543
  }
402
- const maxAttestation = isPostElectra ? MAX_ATTESTATIONS_PER_GROUP_ELECTRA : MAX_ATTESTATIONS_PER_GROUP;
403
- if (attestations.length <= maxAttestation) {
404
- return attestations;
405
- }
406
- return attestations.sort((a, b) => b.notSeenAttesterCount - a.notSeenAttesterCount).slice(0, maxAttestation);
544
+ return mostValuableAttestation;
407
545
  }
408
546
  /** Get attestations for API. */
409
547
  getAttestations() {
@@ -449,13 +587,14 @@ export function aggregateConsolidation({ byCommittee, attData }) {
449
587
  * has already attested or not.
450
588
  */
451
589
  export function getNotSeenValidatorsFn(state) {
452
- if (state.config.getForkName(state.slot) === ForkName.phase0) {
590
+ const stateSlot = state.slot;
591
+ if (state.config.getForkName(stateSlot) === ForkName.phase0) {
453
592
  // Get attestations to be included in a phase0 block.
454
593
  // As we are close to altair, this is not really important, it's mainly for e2e.
455
594
  // The performance is not great due to the different BeaconState data structure to altair.
456
595
  // check for phase0 block already
457
596
  const phase0State = state;
458
- const stateEpoch = computeEpochAtSlot(state.slot);
597
+ const stateEpoch = computeEpochAtSlot(stateSlot);
459
598
  const previousEpochParticipants = extractParticipationPhase0(phase0State.previousEpochAttestations.getAllReadonly(), state);
460
599
  const currentEpochParticipants = extractParticipationPhase0(phase0State.currentEpochAttestations.getAllReadonly(), state);
461
600
  return (epoch, slot, committeeIndex) => {
@@ -464,13 +603,13 @@ export function getNotSeenValidatorsFn(state) {
464
603
  return null;
465
604
  }
466
605
  const committee = state.epochCtx.getBeaconCommittee(slot, committeeIndex);
467
- const notSeenAttestingIndices = new Set();
606
+ const notSeenCommitteeMembers = new Set();
468
607
  for (const [i, validatorIndex] of committee.entries()) {
469
608
  if (!participants.has(validatorIndex)) {
470
- notSeenAttestingIndices.add(i);
609
+ notSeenCommitteeMembers.add(i);
471
610
  }
472
611
  }
473
- return notSeenAttestingIndices.size === 0 ? null : notSeenAttestingIndices;
612
+ return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers;
474
613
  };
475
614
  }
476
615
  // altair and future forks
@@ -481,7 +620,7 @@ export function getNotSeenValidatorsFn(state) {
481
620
  const altairState = state;
482
621
  const previousParticipation = altairState.previousEpochParticipation.getAll();
483
622
  const currentParticipation = altairState.currentEpochParticipation.getAll();
484
- const stateEpoch = computeEpochAtSlot(state.slot);
623
+ const stateEpoch = computeEpochAtSlot(stateSlot);
485
624
  // this function could be called multiple times with same slot + committeeIndex
486
625
  const cachedNotSeenValidators = new Map();
487
626
  return (epoch, slot, committeeIndex) => {
@@ -490,22 +629,23 @@ export function getNotSeenValidatorsFn(state) {
490
629
  return null;
491
630
  }
492
631
  const cacheKey = slot + "_" + committeeIndex;
493
- let notSeenAttestingIndices = cachedNotSeenValidators.get(cacheKey);
494
- if (notSeenAttestingIndices != null) {
632
+ let notSeenCommitteeMembers = cachedNotSeenValidators.get(cacheKey);
633
+ if (notSeenCommitteeMembers != null) {
495
634
  // if all validators are seen then return null, we don't need to check for any attestations of same committee again
496
- return notSeenAttestingIndices.size === 0 ? null : notSeenAttestingIndices;
635
+ return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers;
497
636
  }
498
637
  const committee = state.epochCtx.getBeaconCommittee(slot, committeeIndex);
499
- notSeenAttestingIndices = new Set();
638
+ notSeenCommitteeMembers = new Set();
500
639
  for (const [i, validatorIndex] of committee.entries()) {
501
640
  // no need to check flagIsTimelySource as if validator is not seen, it's participation status is 0
502
- if (participationStatus[validatorIndex] === 0) {
503
- 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);
504
644
  }
505
645
  }
506
- cachedNotSeenValidators.set(cacheKey, notSeenAttestingIndices);
646
+ cachedNotSeenValidators.set(cacheKey, notSeenCommitteeMembers);
507
647
  // if all validators are seen then return null, we don't need to check for any attestations of same committee again
508
- return notSeenAttestingIndices.size === 0 ? null : notSeenAttestingIndices;
648
+ return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers;
509
649
  };
510
650
  }
511
651
  export function extractParticipationPhase0(attestations, state) {
@@ -551,8 +691,9 @@ export function getValidateAttestationDataFn(forkChoice, state) {
551
691
  else {
552
692
  return false;
553
693
  }
554
- if (!ssz.phase0.Checkpoint.equals(attData.source, justifiedCheckpoint))
694
+ if (!ssz.phase0.Checkpoint.equals(attData.source, justifiedCheckpoint)) {
555
695
  return false;
696
+ }
556
697
  // Shuffling can't have changed if we're in the first few epochs
557
698
  // Also we can't look back 2 epochs if target epoch is 1 or less
558
699
  if (stateEpoch < 2 || targetEpoch < 2) {
@@ -618,7 +759,9 @@ function isValidShuffling(forkChoice, state, blockRootHex, targetEpoch) {
618
759
  }
619
760
  let attestationDependentRoot;
620
761
  try {
621
- 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);
622
765
  }
623
766
  catch (_) {
624
767
  // getDependent root may throw error if the dependent root of attestation data is prior to finalized slot