Skip to content

Commit d2a2a3c

Browse files
nflaigensi321
authored andcommitted
fix: remove aggregation bits from seen attestation cache (#7265)
* fix: remove aggregation bits from seen attestation cache * Allow passing null as aggregationBits to test pre-electra case * Only create aggregationBits once for the first attestation * Avoid second getSingleTrueBit call
1 parent b053ded commit d2a2a3c

File tree

6 files changed

+58
-51
lines changed

6 files changed

+58
-51
lines changed

packages/beacon-node/src/api/impl/beacon/pool/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,16 @@ export function getBeaconPoolApi({
105105
// when a validator is configured with multiple beacon node urls, this attestation data may come from another beacon node
106106
// and the block hasn't been in our forkchoice since we haven't seen / processing that block
107107
// see https://github.com/ChainSafe/lodestar/issues/5098
108-
const {indexedAttestation, subnet, attDataRootHex, committeeIndex, aggregationBits} =
108+
const {indexedAttestation, subnet, attDataRootHex, committeeIndex, committeeValidatorIndex, committeeSize} =
109109
await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot);
110110

111111
if (network.shouldAggregate(subnet, slot)) {
112112
const insertOutcome = chain.attestationPool.add(
113113
committeeIndex,
114114
attestation,
115115
attDataRootHex,
116-
aggregationBits
116+
committeeValidatorIndex,
117+
committeeSize
117118
);
118119
metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome});
119120
}

packages/beacon-node/src/chain/opPools/attestationPool.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ export class AttestationPool {
109109
committeeIndex: CommitteeIndex,
110110
attestation: SingleAttestation,
111111
attDataRootHex: RootHex,
112-
aggregationBits: BitArray | null
112+
committeeValidatorIndex: number,
113+
committeeSize: number
113114
): InsertOutcome {
114115
const slot = attestation.data.slot;
115116
const fork = this.config.getForkName(slot);
@@ -149,10 +150,10 @@ export class AttestationPool {
149150
const aggregate = aggregateByIndex.get(committeeIndex);
150151
if (aggregate) {
151152
// Aggregate mutating
152-
return aggregateAttestationInto(aggregate, attestation, aggregationBits);
153+
return aggregateAttestationInto(aggregate, attestation, committeeValidatorIndex);
153154
}
154155
// Create new aggregate
155-
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, aggregationBits));
156+
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, committeeValidatorIndex, committeeSize));
156157
return InsertOutcome.NewData;
157158
}
158159

@@ -224,13 +225,12 @@ export class AttestationPool {
224225
function aggregateAttestationInto(
225226
aggregate: AggregateFast,
226227
attestation: SingleAttestation,
227-
aggregationBits: BitArray | null
228+
committeeValidatorIndex: number
228229
): InsertOutcome {
229230
let bitIndex: number | null;
230231

231232
if (isElectraSingleAttestation(attestation)) {
232-
assert.notNull(aggregationBits, "aggregationBits missing post-electra");
233-
bitIndex = aggregationBits.getSingleTrueBit();
233+
bitIndex = committeeValidatorIndex;
234234
} else {
235235
bitIndex = attestation.aggregationBits.getSingleTrueBit();
236236
}
@@ -250,12 +250,15 @@ function aggregateAttestationInto(
250250
/**
251251
* Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto()
252252
*/
253-
function attestationToAggregate(attestation: SingleAttestation, aggregationBits: BitArray | null): AggregateFast {
253+
function attestationToAggregate(
254+
attestation: SingleAttestation,
255+
committeeValidatorIndex: number,
256+
committeeSize: number
257+
): AggregateFast {
254258
if (isElectraSingleAttestation(attestation)) {
255-
assert.notNull(aggregationBits, "aggregationBits missing post-electra to generate aggregate");
256259
return {
257260
data: attestation.data,
258-
aggregationBits,
261+
aggregationBits: BitArray.fromSingleBit(committeeSize, committeeValidatorIndex),
259262
committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, attestation.committeeIndex),
260263
signature: signatureFromBytesNoCheck(attestation.signature),
261264
};

packages/beacon-node/src/chain/seenCache/seenAttestationData.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ type AttDataBase64 = string;
1111
export type AttestationDataCacheEntry = {
1212
// part of shuffling data, so this does not take memory
1313
committeeValidatorIndices: Uint32Array;
14-
// TODO: remove this? this is available in SingleAttestation
1514
committeeIndex: CommitteeIndex;
1615
// IndexedAttestationData signing root, 32 bytes
1716
signingRoot: Uint8Array;
@@ -21,8 +20,6 @@ export type AttestationDataCacheEntry = {
2120
// for example in a mainnet node subscribing to all subnets, attestations are processed up to 20k per slot
2221
attestationData: phase0.AttestationData;
2322
subnet: number;
24-
// aggregationBits only populates post-electra. Pre-electra can use get it directly from attestationOrBytes
25-
aggregationBits: BitArray | null;
2623
};
2724

2825
export enum RejectReason {

packages/beacon-node/src/chain/validation/attestation.ts

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ export type AttestationValidationResult = {
6969
subnet: number;
7070
attDataRootHex: RootHex;
7171
committeeIndex: CommitteeIndex;
72-
aggregationBits: BitArray | null; // Field populated post-electra only
72+
committeeValidatorIndex: number;
73+
committeeSize: number;
7374
};
7475

7576
export type AttestationOrBytes = ApiAttestation | GossipAttestation;
@@ -325,6 +326,7 @@ async function validateAttestationNoSignatureCheck(
325326
}
326327

327328
let aggregationBits: BitArray | null = null;
329+
let committeeValidatorIndex: number | null = null;
328330
if (!isForkPostElectra(fork)) {
329331
// [REJECT] The attestation is unaggregated -- that is, it has exactly one participating validator
330332
// (len([bit for bit in attestation.aggregation_bits if bit]) == 1, i.e. exactly 1 bit is set).
@@ -344,11 +346,7 @@ async function validateAttestationNoSignatureCheck(
344346
code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET,
345347
});
346348
}
347-
} else {
348-
// Populate aggregationBits if cached post-electra, else we populate later
349-
if (attestationOrCache.cache && attestationOrCache.cache.aggregationBits !== null) {
350-
aggregationBits = attestationOrCache.cache.aggregationBits;
351-
}
349+
committeeValidatorIndex = bitIndex;
352350
}
353351

354352
let committeeValidatorIndices: Uint32Array;
@@ -407,10 +405,9 @@ async function validateAttestationNoSignatureCheck(
407405
if (!isForkPostElectra(fork)) {
408406
// The validity of aggregation bits are already checked above
409407
assert.notNull(aggregationBits);
410-
const bitIndex = aggregationBits.getSingleTrueBit();
411-
assert.notNull(bitIndex);
408+
assert.notNull(committeeValidatorIndex);
412409

413-
validatorIndex = committeeValidatorIndices[bitIndex];
410+
validatorIndex = committeeValidatorIndices[committeeValidatorIndex];
414411
// [REJECT] The number of aggregation bits matches the committee size
415412
// -- i.e. len(attestation.aggregation_bits) == len(get_beacon_committee(state, data.slot, data.index)).
416413
// > TODO: Is this necessary? Lighthouse does not do this check.
@@ -434,17 +431,12 @@ async function validateAttestationNoSignatureCheck(
434431

435432
// [REJECT] The attester is a member of the committee -- i.e.
436433
// `attestation.attester_index in get_beacon_committee(state, attestation.data.slot, index)`.
437-
// If `aggregationBitsElectra` exists, that means we have already cached it. No need to check again
438-
if (aggregationBits === null) {
439-
// Position of the validator in its committee
440-
const committeeValidatorIndex = committeeValidatorIndices.indexOf(validatorIndex);
441-
if (committeeValidatorIndex === -1) {
442-
throw new AttestationError(GossipAction.REJECT, {
443-
code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE,
444-
});
445-
}
446-
447-
aggregationBits = BitArray.fromSingleBit(committeeValidatorIndices.length, committeeValidatorIndex);
434+
// Position of the validator in its committee
435+
committeeValidatorIndex = committeeValidatorIndices.indexOf(validatorIndex);
436+
if (committeeValidatorIndex === -1) {
437+
throw new AttestationError(GossipAction.REJECT, {
438+
code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE,
439+
});
448440
}
449441
}
450442

@@ -517,7 +509,6 @@ async function validateAttestationNoSignatureCheck(
517509
// root of AttestationData was already cached during getIndexedAttestationSignatureSet
518510
attDataRootHex,
519511
attestationData: attData,
520-
aggregationBits: isForkPostElectra(fork) ? aggregationBits : null,
521512
});
522513
}
523514
}
@@ -553,7 +544,8 @@ async function validateAttestationNoSignatureCheck(
553544
signatureSet,
554545
validatorIndex,
555546
committeeIndex,
556-
aggregationBits: isForkPostElectra(fork) ? aggregationBits : null,
547+
committeeValidatorIndex,
548+
committeeSize: committeeValidatorIndices.length,
557549
};
558550
}
559551

packages/beacon-node/src/network/processor/gossipHandlers.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -637,8 +637,14 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp
637637
results.push(null);
638638

639639
// Handler
640-
const {indexedAttestation, attDataRootHex, attestation, committeeIndex, aggregationBits} =
641-
validationResult.result;
640+
const {
641+
indexedAttestation,
642+
attDataRootHex,
643+
attestation,
644+
committeeIndex,
645+
committeeValidatorIndex,
646+
committeeSize,
647+
} = validationResult.result;
642648
metrics?.registerGossipUnaggregatedAttestation(gossipHandlerParams[i].seenTimestampSec, indexedAttestation);
643649

644650
try {
@@ -650,7 +656,8 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp
650656
committeeIndex,
651657
attestation,
652658
attDataRootHex,
653-
aggregationBits
659+
committeeValidatorIndex,
660+
committeeSize
654661
);
655662
metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome});
656663
}

packages/beacon-node/test/unit/chain/opPools/attestationPool.test.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ describe("AttestationPool", () => {
2424
const clockStub = getMockedClock();
2525
vi.spyOn(clockStub, "secFromSlot").mockReturnValue(0);
2626

27+
const committeeValidatorIndex = 0;
28+
const committeeSize = 128;
29+
2730
const cutOffSecFromSlot = (2 / 3) * config.SECONDS_PER_SLOT;
2831

2932
// Mock attestations
@@ -37,10 +40,10 @@ describe("AttestationPool", () => {
3740
signature: validSignature,
3841
};
3942
const electraAttestation: electra.Attestation = {
40-
...ssz.electra.Attestation.defaultValue(),
41-
committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, electraSingleAttestation.committeeIndex),
43+
aggregationBits: BitArray.fromSingleBit(committeeSize, committeeValidatorIndex),
4244
data: electraAttestationData,
4345
signature: validSignature,
46+
committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, electraSingleAttestation.committeeIndex),
4447
};
4548
const phase0AttestationData: phase0.AttestationData = {
4649
...ssz.phase0.AttestationData.defaultValue(),
@@ -60,9 +63,14 @@ describe("AttestationPool", () => {
6063

6164
it("add correct electra attestation", () => {
6265
const committeeIndex = 0;
63-
const aggregationBits = ssz.electra.Attestation.fields.aggregationBits.defaultValue();
6466
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraSingleAttestation.data));
65-
const outcome = pool.add(committeeIndex, electraSingleAttestation, attDataRootHex, aggregationBits);
67+
const outcome = pool.add(
68+
committeeIndex,
69+
electraSingleAttestation,
70+
attDataRootHex,
71+
committeeValidatorIndex,
72+
committeeSize
73+
);
6674

6775
expect(outcome).equal(InsertOutcome.NewData);
6876
expect(pool.getAggregate(electraAttestationData.slot, committeeIndex, attDataRootHex)).toEqual(electraAttestation);
@@ -71,7 +79,7 @@ describe("AttestationPool", () => {
7179
it("add correct phase0 attestation", () => {
7280
const committeeIndex = null;
7381
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0Attestation.data));
74-
const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, null);
82+
const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, committeeValidatorIndex, committeeSize);
7583

7684
expect(outcome).equal(InsertOutcome.NewData);
7785
expect(pool.getAggregate(phase0AttestationData.slot, committeeIndex, attDataRootHex)).toEqual(phase0Attestation);
@@ -82,18 +90,18 @@ describe("AttestationPool", () => {
8290

8391
it("add electra attestation without committee index", () => {
8492
const committeeIndex = null;
85-
const aggregationBits = ssz.electra.Attestation.fields.aggregationBits.defaultValue();
8693
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraSingleAttestation.data));
8794

88-
expect(() => pool.add(committeeIndex, electraSingleAttestation, attDataRootHex, aggregationBits)).toThrow();
95+
expect(() =>
96+
pool.add(committeeIndex, electraSingleAttestation, attDataRootHex, committeeValidatorIndex, committeeSize)
97+
).toThrow();
8998
expect(pool.getAggregate(electraAttestationData.slot, committeeIndex, attDataRootHex)).toBeNull();
9099
});
91100

92101
it("add phase0 attestation with committee index", () => {
93102
const committeeIndex = 0;
94-
const aggregationBits = ssz.electra.Attestation.fields.aggregationBits.defaultValue();
95103
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0Attestation.data));
96-
const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, aggregationBits);
104+
const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, committeeValidatorIndex, committeeSize);
97105

98106
expect(outcome).equal(InsertOutcome.NewData);
99107
expect(pool.getAggregate(phase0AttestationData.slot, committeeIndex, attDataRootHex)).toEqual(phase0Attestation);
@@ -104,15 +112,14 @@ describe("AttestationPool", () => {
104112

105113
it("add electra attestation with phase0 slot", () => {
106114
const electraAttestationDataWithPhase0Slot = {...ssz.phase0.AttestationData.defaultValue(), slot: GENESIS_SLOT};
107-
const aggregationBits = ssz.electra.Attestation.fields.aggregationBits.defaultValue();
108115
const singleAttestation = {
109116
...ssz.electra.SingleAttestation.defaultValue(),
110117
data: electraAttestationDataWithPhase0Slot,
111118
signature: validSignature,
112119
};
113120
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraAttestationDataWithPhase0Slot));
114121

115-
expect(() => pool.add(0, singleAttestation, attDataRootHex, aggregationBits)).toThrow();
122+
expect(() => pool.add(0, singleAttestation, attDataRootHex, committeeValidatorIndex, committeeSize)).toThrow();
116123
});
117124

118125
it("add phase0 attestation with electra slot", () => {
@@ -127,6 +134,6 @@ describe("AttestationPool", () => {
127134
};
128135
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0AttestationDataWithElectraSlot));
129136

130-
expect(() => pool.add(0, attestation, attDataRootHex, null)).toThrow();
137+
expect(() => pool.add(0, attestation, attDataRootHex, committeeValidatorIndex, committeeSize)).toThrow();
131138
});
132139
});

0 commit comments

Comments
 (0)