From 1d74606117c3773a90ec1983c0ec449846af443b Mon Sep 17 00:00:00 2001 From: twoeths Date: Wed, 8 May 2024 18:09:39 +0700 Subject: [PATCH] fix: attestation pool for electra (#6744) * feat: attestationPool to group by slot by data root by committee index for electra * fix: gossip validation and assert.notNull() util * fix: remove light-client stats.html * fix: lint and check-types --- packages/api/src/beacon/routes/validator.ts | 16 +-- .../api/test/unit/beacon/testData/events.ts | 98 ++++++++++--------- .../test/unit/beacon/testData/validator.ts | 4 +- .../src/api/impl/beacon/pool/index.ts | 3 +- .../src/api/impl/validator/index.ts | 11 ++- .../opPools/aggregatedAttestationPool.ts | 8 +- .../src/chain/opPools/attestationPool.ts | 79 ++++++--------- .../src/chain/validation/aggregateAndProof.ts | 2 +- .../src/chain/validation/attestation.ts | 4 +- .../test/perf/epoch/epochAltair.test.ts | 4 +- .../test/perf/epoch/epochCapella.test.ts | 4 +- .../test/perf/epoch/epochPhase0.test.ts | 4 +- .../processEffectiveBalanceUpdates.test.ts | 3 +- .../perf/epoch/processRegistryUpdates.test.ts | 3 +- packages/utils/src/assert.ts | 12 +++ packages/utils/test/unit/assert.test.ts | 12 ++- .../validator/src/services/attestation.ts | 8 +- .../validator/src/services/validatorStore.ts | 3 +- 18 files changed, 149 insertions(+), 129 deletions(-) diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index f7deeee015e5..c7f561d31bd7 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -409,9 +409,9 @@ export type Endpoints = { /** HashTreeRoot of AttestationData that validator want's aggregated */ attestationDataRoot: Root; slot: Slot; - index: number; + committeeIndex: number; }, - {query: {attestation_data_root: string; slot: number; index: number}}, + {query: {attestation_data_root: string; slot: number; committeeIndex: number}}, allForks.Attestation, VersionMeta >; @@ -805,16 +805,20 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({ - query: {attestation_data_root: toHexString(attestationDataRoot), slot, index}, + writeReq: ({attestationDataRoot, slot, committeeIndex}) => ({ + query: {attestation_data_root: toHexString(attestationDataRoot), slot, committeeIndex}, }), parseReq: ({query}) => ({ attestationDataRoot: fromHexString(query.attestation_data_root), slot: query.slot, - index: query.slot, + committeeIndex: query.slot, }), schema: { - query: {attestation_data_root: Schema.StringRequired, slot: Schema.UintRequired, index: Schema.UintRequired}, + query: { + attestation_data_root: Schema.StringRequired, + slot: Schema.UintRequired, + committeeIndex: Schema.UintRequired, + }, }, }, resp: { diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index 8a7610a26836..7966d0ea3b8a 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -31,18 +31,21 @@ export const eventTestData: EventData = { block: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf", executionOptimistic: false, }, - [EventType.attestation]: ssz.phase0.Attestation.fromJson({ - aggregation_bits: "0x01", - signature: - "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", - data: { - slot: "1", - index: "1", - beacon_block_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - source: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}, - target: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}, - }, - }), + [EventType.attestation]: { + version: ForkName.altair, + data: ssz.phase0.Attestation.fromJson({ + aggregation_bits: "0x01", + signature: + "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + data: { + slot: "1", + index: "1", + beacon_block_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + source: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}, + target: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}, + }, + }), + }, [EventType.voluntaryExit]: ssz.phase0.SignedVoluntaryExit.fromJson({ message: {epoch: "1", validator_index: "1"}, signature: @@ -72,44 +75,47 @@ export const eventTestData: EventData = { "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, }), - [EventType.attesterSlashing]: ssz.phase0.AttesterSlashing.fromJson({ - attestation_1: { - attesting_indices: ["0", "1"], - data: { - slot: "0", - index: "0", - beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", - source: { - epoch: "0", - root: "0x0000000000000000000000000000000000000000000000000000000000000000", - }, - target: { - epoch: "0", - root: "0x0000000000000000000000000000000000000000000000000000000000000000", + [EventType.attesterSlashing]: { + version: ForkName.altair, + data: ssz.phase0.AttesterSlashing.fromJson({ + attestation_1: { + attesting_indices: ["0", "1"], + data: { + slot: "0", + index: "0", + beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + source: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + target: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, }, + signature: + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, - signature: - "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - }, - attestation_2: { - attesting_indices: ["0", "1"], - data: { - slot: "0", - index: "0", - beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", - source: { - epoch: "0", - root: "0x0000000000000000000000000000000000000000000000000000000000000000", - }, - target: { - epoch: "0", - root: "0x0000000000000000000000000000000000000000000000000000000000000000", + attestation_2: { + attesting_indices: ["0", "1"], + data: { + slot: "0", + index: "0", + beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + source: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + target: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, }, + signature: + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, - signature: - "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - }, - }), + }), + }, [EventType.blsToExecutionChange]: ssz.capella.SignedBLSToExecutionChange.fromJson({ message: { validator_index: "1", diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index 11fd7dd26425..5f82d4a818b0 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -99,8 +99,8 @@ export const testData: GenericServerTestCases = { res: {data: ssz.altair.SyncCommitteeContribution.defaultValue()}, }, getAggregatedAttestation: { - args: {attestationDataRoot: ZERO_HASH, slot: 32000}, - res: {data: ssz.phase0.Attestation.defaultValue()}, + args: {attestationDataRoot: ZERO_HASH, slot: 32000, index: 2}, + res: {data: ssz.phase0.Attestation.defaultValue(), meta: {version: ForkName.phase0}}, }, publishAggregateAndProofs: { args: {signedAggregateAndProofs: [ssz.phase0.SignedAggregateAndProof.defaultValue()]}, diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 77f6b24f28af..3f594fa4c3ae 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -27,12 +27,13 @@ export function getBeaconPoolApi({ async getPoolAttestations({slot, committeeIndex}) { // Already filtered by slot let attestations = chain.aggregatedAttestationPool.getAll(slot); + const fork = chain.config.getForkName(slot ?? attestations[0].data.slot) ?? ForkName.phase0; if (committeeIndex !== undefined) { attestations = attestations.filter((attestation) => committeeIndex === attestation.data.index); } - return {data: attestations}; + return {data: attestations, meta: {version: fork}}; }, async getPoolAttesterSlashings() { diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 764b201af140..903cb25aff0c 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1063,23 +1063,26 @@ export function getValidatorApi({ }; }, - async getAggregatedAttestation({attestationDataRoot, slot}) { + async getAggregatedAttestation({attestationDataRoot, slot, committeeIndex}) { notWhileSyncing(); await waitForSlot(slot); // Must never request for a future slot > currentSlot const dataRootHex = toHex(attestationDataRoot); - const aggregate = chain.attestationPool.getAggregate(slot, dataRootHex); + const aggregate = chain.attestationPool.getAggregate(slot, committeeIndex, dataRootHex); if (!aggregate) { - throw new ApiError(404, `No aggregated attestation for slot=${slot}, dataRoot=${dataRootHex}`); + throw new ApiError( + 404, + `No aggregated attestation for slot=${slot} committeeIndex=${committeeIndex}, dataRoot=${dataRootHex}` + ); } metrics?.production.producedAggregateParticipants.observe(aggregate.aggregationBits.getTrueBitIndexes().length); return { data: aggregate, - version: config.getForkName(slot), + meta: {version: config.getForkName(slot)}, }; }, diff --git a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts index d6cceb9572ae..fde1ac88ace0 100644 --- a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts @@ -30,7 +30,7 @@ import { getBlockRootAtSlot, } from "@lodestar/state-transition"; import {IForkChoice, EpochDifference} from "@lodestar/fork-choice"; -import {toHex, MapDef} from "@lodestar/utils"; +import {toHex, MapDef, assert} from "@lodestar/utils"; import {intersectUint8Arrays, IntersectResult} from "../../util/bitArray.js"; import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; import {InsertOutcome} from "./types.js"; @@ -141,10 +141,8 @@ export class AggregatedAttestationPool { ? // this attestation is added to pool after validation attestation.committeeBits.getSingleTrueBit() : attestation.data.index; - if (committeeIndex === null) { - // this should not happen because attestation should be validated before reaching this - throw Error(`Invalid attestation slot=${slot} committeeIndex=${committeeIndex}`); - } + // this should not happen because attestation should be validated before reaching this + assert.notNull(committeeIndex, "Committee index should not be null in aggregated attestation pool"); let attestationGroup = attestationGroupByIndex.get(committeeIndex); if (!attestationGroup) { attestationGroup = new MatchingDataAttestationGroup(committee, attestation.data); diff --git a/packages/beacon-node/src/chain/opPools/attestationPool.ts b/packages/beacon-node/src/chain/opPools/attestationPool.ts index 38e910753440..7d6cca30056c 100644 --- a/packages/beacon-node/src/chain/opPools/attestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/attestationPool.ts @@ -1,8 +1,8 @@ import {PointFormat, Signature} from "@chainsafe/bls/types"; import bls from "@chainsafe/bls"; import {BitArray} from "@chainsafe/ssz"; -import {Slot, RootHex, allForks} from "@lodestar/types"; -import {MapDef} from "@lodestar/utils"; +import {Slot, RootHex, allForks, isElectraAttestation} from "@lodestar/types"; +import {MapDef, assert} from "@lodestar/utils"; import {IClock} from "../../util/clock.js"; import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js"; import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; @@ -36,6 +36,8 @@ type AggregateFast = AggregateFastPhase0 | AggregateFastElectra; /** Hex string of DataRoot `TODO` */ type DataRootHex = string; +type CommitteeIndex = number; + /** * A pool of `Attestation` that is specially designed to store "unaggregated" attestations from * the native aggregation scheme. @@ -60,8 +62,8 @@ type DataRootHex = string; * receives and it can be triggered manually. */ export class AttestationPool { - private readonly attestationByRootBySlot = new MapDef>( - () => new Map() + private readonly attestationByRootBySlot = new MapDef>>( + () => new Map>() ); private lowestPermissibleSlot = 0; @@ -117,14 +119,26 @@ export class AttestationPool { throw new OpPoolError({code: OpPoolErrorCode.REACHED_MAX_PER_SLOT}); } + const committeeIndex = isElectraAttestation(attestation) + ? // this attestation is added to pool after validation + attestation.committeeBits.getSingleTrueBit() + : attestation.data.index; + // this should not happen because attestation should be validated before reaching this + assert.notNull(committeeIndex, "Committee index should not be null in attestation pool"); + // Pre-aggregate the contribution with existing items - const aggregate = aggregateByRoot.get(attDataRootHex); + let aggregateByIndex = aggregateByRoot.get(attDataRootHex); + if (aggregateByIndex === undefined) { + aggregateByIndex = new Map(); + aggregateByRoot.set(attDataRootHex, aggregateByIndex); + } + const aggregate = aggregateByIndex.get(committeeIndex); if (aggregate) { // Aggregate mutating return aggregateAttestationInto(aggregate, attestation); } else { // Create new aggregate - aggregateByRoot.set(attDataRootHex, attestationToAggregate(attestation)); + aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation)); return InsertOutcome.NewData; } } @@ -132,8 +146,8 @@ export class AttestationPool { /** * For validator API to get an aggregate */ - getAggregate(slot: Slot, dataRootHex: RootHex): allForks.Attestation | null { - const aggregate = this.attestationByRootBySlot.get(slot)?.get(dataRootHex); + getAggregate(slot: Slot, committeeIndex: CommitteeIndex, dataRootHex: RootHex): allForks.Attestation | null { + const aggregate = this.attestationByRootBySlot.get(slot)?.get(dataRootHex)?.get(committeeIndex); if (!aggregate) { // TODO: Add metric for missing aggregates return null; @@ -166,8 +180,10 @@ export class AttestationPool { for (const aggregateByRoot of aggregateByRoots) { if (aggregateByRoot) { - for (const aggFast of aggregateByRoot.values()) { - attestations.push(fastToAttestation(aggFast)); + for (const aggFastByIndex of aggregateByRoot.values()) { + for (const aggFast of aggFastByIndex.values()) { + attestations.push(fastToAttestation(aggFast)); + } } } } @@ -180,35 +196,13 @@ export class AttestationPool { // - Insert attestations coming from gossip and API /** - * Aggregate a new contribution into `aggregate` mutating it + * Aggregate a new attestation into `aggregate` mutating it */ function aggregateAttestationInto(aggregate: AggregateFast, attestation: allForks.Attestation): InsertOutcome { const bitIndex = attestation.aggregationBits.getSingleTrueBit(); // Should never happen, attestations are verified against this exact condition before - if (bitIndex === null) { - throw Error("Invalid attestation not exactly one bit set"); - } - - if ("committeeBits" in attestation && !("committeeBits" in aggregate)) { - throw Error("Attempt to aggregate electra attestation into phase0 attestation"); - } - - if (!("committeeBits" in attestation) && "committeeBits" in aggregate) { - throw Error("Attempt to aggregate phase0 attestation into electra attestation"); - } - - if ("committeeBits" in attestation) { - // We assume attestation.committeeBits should already be validated in api and gossip handler and should be non-null - const attestationCommitteeIndex = attestation.committeeBits.getSingleTrueBit(); - const aggregateCommitteeIndex = (aggregate as AggregateFastElectra).committeeBits.getSingleTrueBit(); - - if (attestationCommitteeIndex !== aggregateCommitteeIndex) { - throw Error( - `Committee index mismatched: attestation ${attestationCommitteeIndex} aggregate ${aggregateCommitteeIndex}` - ); - } - } + assert.notNull(bitIndex, "Invalid attestation in pool, not exactly one bit set"); if (aggregate.aggregationBits.get(bitIndex) === true) { return InsertOutcome.AlreadyKnown; @@ -226,7 +220,7 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: allFork * Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto() */ function attestationToAggregate(attestation: allForks.Attestation): AggregateFast { - if ("committeeBits" in attestation) { + if (isElectraAttestation(attestation)) { return { data: attestation.data, // clone because it will be mutated @@ -247,18 +241,5 @@ function attestationToAggregate(attestation: allForks.Attestation): AggregateFas * Unwrap AggregateFast to phase0.Attestation */ function fastToAttestation(aggFast: AggregateFast): allForks.Attestation { - if ("committeeBits" in aggFast) { - return { - data: aggFast.data, - aggregationBits: aggFast.aggregationBits, - committeeBits: aggFast.committeeBits, - signature: aggFast.signature.toBytes(PointFormat.compressed), - }; - } else { - return { - data: aggFast.data, - aggregationBits: aggFast.aggregationBits, - signature: aggFast.signature.toBytes(PointFormat.compressed), - }; - } + return {...aggFast, signature: aggFast.signature.toBytes(PointFormat.compressed)}; } diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 7950466570ce..a1d7135716ce 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -82,7 +82,7 @@ async function validateAggregateAndProof( throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}); } // [REJECT] aggregate.data.index == 0 - if (attData.index === 0) { + if (attData.index !== 0) { throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); } } else { diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index a186b1325396..87e8c2a6b2bd 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -44,7 +44,7 @@ export type AttestationValidationResult = { export type AttestationOrBytes = ApiAttestation | GossipAttestation; /** attestation from api */ -export type ApiAttestation = {attestation: phase0.Attestation; serializedData: null}; // TODO Electra: add new attestation type +export type ApiAttestation = {attestation: phase0.Attestation; serializedData: null}; /** attestation from gossip */ export type GossipAttestation = { @@ -298,7 +298,7 @@ async function validateGossipAttestationNoSignatureCheck( } // [REJECT] aggregate.data.index == 0 - if (attData.index === 0) { + if (attData.index !== 0) { throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); } } else { diff --git a/packages/state-transition/test/perf/epoch/epochAltair.test.ts b/packages/state-transition/test/perf/epoch/epochAltair.test.ts index 273353d8632b..6c43151cc137 100644 --- a/packages/state-transition/test/perf/epoch/epochAltair.test.ts +++ b/packages/state-transition/test/perf/epoch/epochAltair.test.ts @@ -120,7 +120,7 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - altair processRegistryUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processRegistryUpdates(state, cache.value), + fn: (state) => processRegistryUpdates(ForkSeq.altair, state, cache.value), }); // TODO: Needs a better state to test with, current does not include enough actions: 39.985 us/op @@ -141,7 +141,7 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - altair processEffectiveBalanceUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processEffectiveBalanceUpdates(state, cache.value), + fn: (state) => processEffectiveBalanceUpdates(ForkSeq.altair, state, cache.value), }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/epochCapella.test.ts b/packages/state-transition/test/perf/epoch/epochCapella.test.ts index eeaf8bfc5400..5b8300df3f18 100644 --- a/packages/state-transition/test/perf/epoch/epochCapella.test.ts +++ b/packages/state-transition/test/perf/epoch/epochCapella.test.ts @@ -99,7 +99,7 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - capella processRegistryUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processRegistryUpdates(state, cache.value), + fn: (state) => processRegistryUpdates(ForkSeq.capella, state, cache.value), }); // TODO: Needs a better state to test with, current does not include enough actions: 39.985 us/op @@ -120,7 +120,7 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - capella processEffectiveBalanceUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processEffectiveBalanceUpdates(state, cache.value), + fn: (state) => processEffectiveBalanceUpdates(ForkSeq.capella, state, cache.value), }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/epochPhase0.test.ts b/packages/state-transition/test/perf/epoch/epochPhase0.test.ts index 4e43634b1669..411577878102 100644 --- a/packages/state-transition/test/perf/epoch/epochPhase0.test.ts +++ b/packages/state-transition/test/perf/epoch/epochPhase0.test.ts @@ -102,7 +102,7 @@ function benchmarkPhase0EpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - phase0 processRegistryUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processRegistryUpdates(state, cache.value), + fn: (state) => processRegistryUpdates(ForkSeq.phase0, state, cache.value), }); // TODO: Needs a better state to test with, current does not include enough actions: 39.985 us/op @@ -123,7 +123,7 @@ function benchmarkPhase0EpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - phase0 processEffectiveBalanceUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processEffectiveBalanceUpdates(state, cache.value), + fn: (state) => processEffectiveBalanceUpdates(ForkSeq.phase0, state, cache.value), }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/processEffectiveBalanceUpdates.test.ts b/packages/state-transition/test/perf/epoch/processEffectiveBalanceUpdates.test.ts index 0fb1d448142f..d94daac9e59b 100644 --- a/packages/state-transition/test/perf/epoch/processEffectiveBalanceUpdates.test.ts +++ b/packages/state-transition/test/perf/epoch/processEffectiveBalanceUpdates.test.ts @@ -1,6 +1,7 @@ import {itBench} from "@dapplion/benchmark"; import {ssz} from "@lodestar/types"; import {config} from "@lodestar/config/default"; +import {ForkSeq} from "@lodestar/params"; import {beforeProcessEpoch, CachedBeaconStateAllForks, EpochTransitionCache} from "../../../src/index.js"; import {processEffectiveBalanceUpdates} from "../../../src/epoch/processEffectiveBalanceUpdates.js"; import {numValidators} from "../util.js"; @@ -35,7 +36,7 @@ describe("phase0 processEffectiveBalanceUpdates", () => { minRuns: 5, // Worst case is very slow before: () => getEffectiveBalanceTestData(vc, changeRatio), beforeEach: ({state, cache}) => ({state: state.clone(), cache}), - fn: ({state, cache}) => processEffectiveBalanceUpdates(state, cache), + fn: ({state, cache}) => processEffectiveBalanceUpdates(ForkSeq.phase0, state, cache), }); } }); diff --git a/packages/state-transition/test/perf/epoch/processRegistryUpdates.test.ts b/packages/state-transition/test/perf/epoch/processRegistryUpdates.test.ts index ccfd2405a665..2d57de44f8ee 100644 --- a/packages/state-transition/test/perf/epoch/processRegistryUpdates.test.ts +++ b/packages/state-transition/test/perf/epoch/processRegistryUpdates.test.ts @@ -1,4 +1,5 @@ import {itBench} from "@dapplion/benchmark"; +import {ForkSeq} from "@lodestar/params"; import {beforeProcessEpoch, CachedBeaconStateAllForks, EpochTransitionCache} from "../../../src/index.js"; import {processRegistryUpdates} from "../../../src/epoch/processRegistryUpdates.js"; import {generatePerfTestCachedStatePhase0, numValidators} from "../util.js"; @@ -62,7 +63,7 @@ describe("phase0 processRegistryUpdates", () => { noThreshold: notTrack, before: () => getRegistryUpdatesTestData(vc, lengths), beforeEach: async ({state, cache}) => ({state: state.clone(), cache}), - fn: ({state, cache}) => processRegistryUpdates(state, cache), + fn: ({state, cache}) => processRegistryUpdates(ForkSeq.phase0, state, cache), }); } }); diff --git a/packages/utils/src/assert.ts b/packages/utils/src/assert.ts index aa86161cca44..91612b0e6407 100644 --- a/packages/utils/src/assert.ts +++ b/packages/utils/src/assert.ts @@ -21,6 +21,18 @@ export const assert = { } }, + /** + * Assert not null + * ``` + * actual !== null + * ``` + */ + notNull(actual: T | null, message?: string): asserts actual is T { + if (!(actual !== null)) { + throw new AssertionError(`${message || "Expected value to be not null"}`); + } + }, + /** * Assert less than or equal * ```js diff --git a/packages/utils/test/unit/assert.test.ts b/packages/utils/test/unit/assert.test.ts index 0555bcbd01a0..3b413efa11be 100644 --- a/packages/utils/test/unit/assert.test.ts +++ b/packages/utils/test/unit/assert.test.ts @@ -20,8 +20,18 @@ describe("assert", () => { }); }); + describe("notNull with custom message", () => { + it("Should not throw error with not null value", () => { + expect(() => assert.notNull(0)).not.toThrow(); + expect(() => assert.notNull("")).not.toThrow(); + }); + it("Should throw with null value", () => { + expect(() => assert.notNull(null, "something must not be null")).toThrow("something must not be null"); + }); + }); + const cases: { - op: keyof Omit; + op: keyof Omit; args: [number, number]; ok: boolean; }[] = [ diff --git a/packages/validator/src/services/attestation.ts b/packages/validator/src/services/attestation.ts index 39c3cfac641d..8f5228e3a10e 100644 --- a/packages/validator/src/services/attestation.ts +++ b/packages/validator/src/services/attestation.ts @@ -128,7 +128,7 @@ export class AttestationService { // Then download, sign and publish a `SignedAggregateAndProof` for each // validator that is elected to aggregate for this `slot` and `committeeIndex`. - await this.produceAndPublishAggregates(attestation, dutiesSameCommittee); + await this.produceAndPublishAggregates(attestation, index, dutiesSameCommittee); } private async runAttestationTasksGrouped( @@ -154,7 +154,7 @@ export class AttestationService { await Promise.all( Array.from(dutiesByCommitteeIndex.entries()).map(([index, dutiesSameCommittee]) => { const attestationData: phase0.AttestationData = {...attestationNoCommittee, index}; - return this.produceAndPublishAggregates(attestationData, dutiesSameCommittee); + return this.produceAndPublishAggregates(attestationData, index, dutiesSameCommittee); }) ); } @@ -245,9 +245,10 @@ export class AttestationService { */ private async produceAndPublishAggregates( attestation: phase0.AttestationData, + committeeIndex: number, duties: AttDutyAndProof[] ): Promise { - const logCtx = {slot: attestation.slot, index: attestation.index}; + const logCtx = {slot: attestation.slot, index: committeeIndex}; // No validator is aggregator, skip if (duties.every(({selectionProof}) => selectionProof === null)) { @@ -258,6 +259,7 @@ export class AttestationService { const res = await this.api.validator.getAggregatedAttestation({ attestationDataRoot: ssz.phase0.AttestationData.hashTreeRoot(attestation), slot: attestation.slot, + committeeIndex, }); const aggregate = res.value(); this.metrics?.numParticipantsInAggregate.observe(aggregate.aggregationBits.getTrueBitIndexes().length); diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 015adfa69080..6c6870acfe4e 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -20,6 +20,7 @@ import { DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, DOMAIN_APPLICATION_BUILDER, ForkSeq, + MAX_COMMITTEES_PER_SLOT, } from "@lodestar/params"; import { altair, @@ -531,7 +532,7 @@ export class ValidatorStore { return { aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), data: attestationData, - committeeBits: BitArray.fromSingleBit(duty.committeesAtSlot, duty.committeeIndex), + committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, duty.committeeIndex), signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), } as electra.Attestation; } else {