@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.
- package/lib/api/impl/beacon/pool/index.js +1 -1
- package/lib/api/impl/beacon/pool/index.js.map +1 -1
- package/lib/api/impl/validator/index.js +5 -4
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/archiveStore/historicalState/worker.js +12 -0
- package/lib/chain/archiveStore/historicalState/worker.js.map +1 -1
- package/lib/chain/chain.js +5 -7
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/opPools/aggregatedAttestationPool.d.ts +32 -14
- package/lib/chain/opPools/aggregatedAttestationPool.js +235 -97
- package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
- package/lib/chain/opPools/attestationPool.d.ts +4 -2
- package/lib/chain/opPools/attestationPool.js +4 -3
- package/lib/chain/opPools/attestationPool.js.map +1 -1
- package/lib/metrics/metrics/beacon.js +1 -1
- package/lib/metrics/metrics/beacon.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +62 -10
- package/lib/metrics/metrics/lodestar.js +141 -28
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/metrics/options.d.ts +1 -1
- package/lib/metrics/validatorMonitor.js +1 -1
- package/lib/metrics/validatorMonitor.js.map +1 -1
- package/lib/network/core/networkCore.d.ts +3 -3
- package/lib/network/core/networkCore.js +9 -4
- package/lib/network/core/networkCore.js.map +1 -1
- package/lib/network/core/networkCoreWorker.js +5 -3
- package/lib/network/core/networkCoreWorker.js.map +1 -1
- package/lib/network/core/networkCoreWorkerHandler.d.ts +2 -2
- package/lib/network/core/networkCoreWorkerHandler.js +3 -3
- package/lib/network/core/networkCoreWorkerHandler.js.map +1 -1
- package/lib/network/core/types.d.ts +1 -1
- package/lib/network/discv5/index.d.ts +2 -3
- package/lib/network/discv5/index.js +4 -5
- package/lib/network/discv5/index.js.map +1 -1
- package/lib/network/discv5/types.d.ts +1 -1
- package/lib/network/discv5/worker.js +7 -6
- package/lib/network/discv5/worker.js.map +1 -1
- package/lib/network/gossip/gossipsub.js +4 -0
- package/lib/network/gossip/gossipsub.js.map +1 -1
- package/lib/network/interface.d.ts +3 -2
- package/lib/network/libp2p/index.d.ts +2 -2
- package/lib/network/libp2p/index.js +9 -9
- package/lib/network/libp2p/index.js.map +1 -1
- package/lib/network/network.d.ts +4 -4
- package/lib/network/network.js +7 -5
- package/lib/network/network.js.map +1 -1
- package/lib/network/peers/datastore.d.ts +2 -1
- package/lib/network/peers/datastore.js +1 -1
- package/lib/network/peers/datastore.js.map +1 -1
- package/lib/network/peers/discover.d.ts +2 -0
- package/lib/network/peers/discover.js +3 -4
- package/lib/network/peers/discover.js.map +1 -1
- package/lib/network/peers/peerManager.d.ts +2 -1
- package/lib/network/peers/peerManager.js +1 -1
- package/lib/network/peers/peerManager.js.map +1 -1
- package/lib/network/peers/utils/getConnectedPeerIds.js +2 -2
- package/lib/network/peers/utils/getConnectedPeerIds.js.map +1 -1
- package/lib/network/processor/gossipHandlers.js +3 -2
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/network/util.d.ts +4 -1
- package/lib/network/util.js +2 -2
- package/lib/network/util.js.map +1 -1
- package/lib/node/nodejs.d.ts +3 -3
- package/lib/node/nodejs.js +2 -2
- package/lib/node/nodejs.js.map +1 -1
- package/lib/util/peerId.js +1 -1
- package/lib/util/peerId.js.map +1 -1
- 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,
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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 =
|
|
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
|
|
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
|
|
154
|
+
const inclusionDistance = stateSlot - slot;
|
|
150
155
|
for (const attestationGroupByIndex of attestationGroupByIndexByDataHash.values()) {
|
|
151
156
|
for (const [committeeIndex, attestationGroup] of attestationGroupByIndex.entries()) {
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
|
216
|
-
let
|
|
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
|
|
234
|
-
|
|
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
|
|
243
|
-
if (
|
|
244
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
288
|
+
totalNewSeenEffectiveBalance: 0,
|
|
289
|
+
newSeenAttesters: 0,
|
|
290
|
+
notSeenAttesters: 0,
|
|
291
|
+
totalAttesters: 0,
|
|
273
292
|
};
|
|
274
293
|
}
|
|
275
|
-
sameAttDataCons[i]
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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(
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
525
|
+
for (const notSeenIndex of notSeenCommitteeMembers) {
|
|
399
526
|
if (aggregationBits.get(notSeenIndex)) {
|
|
400
|
-
|
|
527
|
+
newSeenEffectiveBalance += effectiveBalanceIncrements[this.committee[notSeenIndex]];
|
|
528
|
+
newSeenAttesters++;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
notSeen.add(notSeenIndex);
|
|
401
532
|
}
|
|
402
533
|
}
|
|
403
|
-
if (
|
|
404
|
-
|
|
534
|
+
if (newSeenEffectiveBalance > maxNewSeenEffectiveBalance) {
|
|
535
|
+
maxNewSeenEffectiveBalance = newSeenEffectiveBalance;
|
|
536
|
+
mostValuableAttestation = {
|
|
537
|
+
attestation,
|
|
538
|
+
newSeenEffectiveBalance,
|
|
539
|
+
newSeenAttesters,
|
|
540
|
+
notSeenCommitteeMembers: notSeen,
|
|
541
|
+
};
|
|
405
542
|
}
|
|
406
543
|
}
|
|
407
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
606
|
+
const notSeenCommitteeMembers = new Set();
|
|
473
607
|
for (const [i, validatorIndex] of committee.entries()) {
|
|
474
608
|
if (!participants.has(validatorIndex)) {
|
|
475
|
-
|
|
609
|
+
notSeenCommitteeMembers.add(i);
|
|
476
610
|
}
|
|
477
611
|
}
|
|
478
|
-
return
|
|
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(
|
|
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
|
|
499
|
-
if (
|
|
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
|
|
635
|
+
return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers;
|
|
502
636
|
}
|
|
503
637
|
const committee = state.epochCtx.getBeaconCommittee(slot, committeeIndex);
|
|
504
|
-
|
|
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
|
-
|
|
508
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|