diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index a9de7e49d9fa..0d5b3a4e60f7 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -444,7 +444,7 @@ export class ForkChoice implements IForkChoice { * The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it * will not be run here. */ - onAttestation(attestation: phase0.IndexedAttestation): void { + onAttestation(attestation: phase0.IndexedAttestation, attDataRoot?: string): void { // Ignore any attestations to the zero hash. // // This is an edge case that results from the spec aliasing the zero hash to the genesis @@ -466,7 +466,7 @@ export class ForkChoice implements IForkChoice { return; } - this.validateOnAttestation(attestation, slot, blockRootHex, targetEpoch); + this.validateOnAttestation(attestation, slot, blockRootHex, targetEpoch, attDataRoot); if (slot < this.fcStore.currentSlot) { for (const validatorIndex of attestation.attestingIndices) { @@ -789,7 +789,8 @@ export class ForkChoice implements IForkChoice { indexedAttestation: phase0.IndexedAttestation, slot: Slot, blockRootHex: string, - targetEpoch: Epoch + targetEpoch: Epoch, + attDataRoot?: string ): void { // There is no point in processing an attestation with an empty bitfield. Reject // it immediately. @@ -807,7 +808,7 @@ export class ForkChoice implements IForkChoice { const attestationData = indexedAttestation.data; // AttestationData is expected to internally cache its root to make this hashTreeRoot() call free - const attestationCacheKey = toHexString(ssz.phase0.AttestationData.hashTreeRoot(attestationData)); + const attestationCacheKey = attDataRoot ?? toHexString(ssz.phase0.AttestationData.hashTreeRoot(attestationData)); if (!this.validatedAttestationDatas.has(attestationCacheKey)) { this.validateAttestationData(indexedAttestation.data, slot, blockRootHex, targetEpoch, attestationCacheKey); diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 8e05189aaecb..89625d6c365c 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -77,7 +77,7 @@ export interface IForkChoice { * The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it * will not be run here. */ - onAttestation(attestation: phase0.IndexedAttestation): void; + onAttestation(attestation: phase0.IndexedAttestation, attDataRoot?: string): void; getLatestMessage(validatorIndex: ValidatorIndex): ILatestMessage | undefined; /** * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`. diff --git a/packages/lodestar/src/chain/blocks/importBlock.ts b/packages/lodestar/src/chain/blocks/importBlock.ts index dc8b569961bd..352adbf9d9c5 100644 --- a/packages/lodestar/src/chain/blocks/importBlock.ts +++ b/packages/lodestar/src/chain/blocks/importBlock.ts @@ -1,3 +1,4 @@ +import {ssz} from "@chainsafe/lodestar-types"; import {SLOTS_PER_EPOCH} from "@chainsafe/lodestar-params"; import {toHexString} from "@chainsafe/ssz"; import {allForks} from "@chainsafe/lodestar-types"; @@ -23,7 +24,7 @@ import {LightClientServer} from "../lightClient"; import {getCheckpointFromState} from "./utils/checkpoint"; import {PendingEvents} from "./utils/pendingEvents"; import {FullyVerifiedBlock} from "./types"; -// import {ForkChoiceError, ForkChoiceErrorCode} from "@chainsafe/lodestar-fork-choice/lib/forkChoice/errors"; +import {SeenAggregatedAttestations} from "../seenCache/seenAggregateAndProof"; /** * Fork-choice allows to import attestations from current (0) or past (1) epoch. @@ -35,6 +36,7 @@ export type ImportBlockModules = { forkChoice: IForkChoice; stateCache: StateContextCache; checkpointStateCache: CheckpointStateCache; + seenAggregatedAttestations: SeenAggregatedAttestations; lightClientServer: LightClientServer; executionEngine: IExecutionEngine; emitter: ChainEventEmitter; @@ -120,10 +122,17 @@ export async function importBlock(chain: ImportBlockModules, fullyVerifiedBlock: const indexedAttestation = postState.epochCtx.getIndexedAttestation(attestation); const targetEpoch = attestation.data.target.epoch; + const attDataRoot = toHexString(ssz.phase0.AttestationData.hashTreeRoot(indexedAttestation.data)); + chain.seenAggregatedAttestations.add( + targetEpoch, + attDataRoot, + {aggregationBits: attestation.aggregationBits, trueBitCount: indexedAttestation.attestingIndices.length}, + true + ); // Duplicated logic from fork-choice onAttestation validation logic. // Attestations outside of this range will be dropped as Errors, so no need to import if (targetEpoch <= currentEpoch && targetEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT) { - chain.forkChoice.onAttestation(indexedAttestation); + chain.forkChoice.onAttestation(indexedAttestation, attDataRoot); } if (parentSlot !== undefined) { diff --git a/packages/lodestar/src/chain/chain.ts b/packages/lodestar/src/chain/chain.ts index 892cc04f9e40..b89b81c202cf 100644 --- a/packages/lodestar/src/chain/chain.ts +++ b/packages/lodestar/src/chain/chain.ts @@ -51,6 +51,7 @@ import {IEth1ForBlockProduction} from "../eth1"; import {IExecutionEngine} from "../executionEngine"; import {PrecomputeNextEpochTransitionScheduler} from "./precomputeNextEpochTransition"; import {ReprocessController} from "./reprocess"; +import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof"; export class BeaconChain implements IBeaconChain { readonly genesisTime: UintNum64; @@ -81,9 +82,10 @@ export class BeaconChain implements IBeaconChain { // Gossip seen cache readonly seenAttesters = new SeenAttesters(); readonly seenAggregators = new SeenAggregators(); + readonly seenAggregatedAttestations: SeenAggregatedAttestations; readonly seenBlockProposers = new SeenBlockProposers(); readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); - readonly seenContributionAndProof = new SeenContributionAndProof(); + readonly seenContributionAndProof: SeenContributionAndProof; // Global state caches readonly pubkey2index: PubkeyIndexMap; @@ -139,6 +141,9 @@ export class BeaconChain implements IBeaconChain { const stateCache = new StateContextCache({metrics}); const checkpointStateCache = new CheckpointStateCache({metrics}); + this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); + this.seenContributionAndProof = new SeenContributionAndProof(metrics); + // Initialize single global instance of state caches this.pubkey2index = new PubkeyIndexMap(); this.index2pubkey = []; @@ -188,6 +193,7 @@ export class BeaconChain implements IBeaconChain { lightClientServer, stateCache, checkpointStateCache, + seenAggregatedAttestations: this.seenAggregatedAttestations, emitter, config, logger, diff --git a/packages/lodestar/src/chain/errors/attestationError.ts b/packages/lodestar/src/chain/errors/attestationError.ts index 44877432493f..a9a26b2db5b1 100644 --- a/packages/lodestar/src/chain/errors/attestationError.ts +++ b/packages/lodestar/src/chain/errors/attestationError.ts @@ -37,6 +37,10 @@ export enum AttestationErrorCode { * There has already been an aggregation observed for this validator, we refuse to process a second. */ AGGREGATOR_ALREADY_KNOWN = "ATTESTATION_ERROR_AGGREGATOR_ALREADY_KNOWN", + /** + * All of the attesters are known, we refuse to process subset of attesting indices since it brings no value. + */ + ATTESTERS_ALREADY_KNOWN = "ATTESTATION_ERROR_ATTESTERS_ALREADY_KNOWN", /** * The aggregator index is higher than the maximum possible validator count. */ @@ -133,6 +137,7 @@ export type AttestationErrorType = | {code: AttestationErrorCode.AGGREGATOR_PUBKEY_UNKNOWN; aggregatorIndex: ValidatorIndex} | {code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN; targetEpoch: Epoch; validatorIndex: number} | {code: AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN; targetEpoch: Epoch; aggregatorIndex: number} + | {code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN; targetEpoch: Epoch; aggregateRoot: RootHex} | {code: AttestationErrorCode.AGGREGATOR_INDEX_TOO_HIGH; aggregatorIndex: ValidatorIndex} | {code: AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT; root: RootHex} | {code: AttestationErrorCode.BAD_TARGET_EPOCH} diff --git a/packages/lodestar/src/chain/errors/syncCommitteeError.ts b/packages/lodestar/src/chain/errors/syncCommitteeError.ts index 39a544c37648..3f203adf6fd6 100644 --- a/packages/lodestar/src/chain/errors/syncCommitteeError.ts +++ b/packages/lodestar/src/chain/errors/syncCommitteeError.ts @@ -4,7 +4,8 @@ import {GossipActionError} from "./gossipValidation"; export enum SyncCommitteeErrorCode { NOT_CURRENT_SLOT = "SYNC_COMMITTEE_ERROR_NOT_CURRENT_SLOT", UNKNOWN_BEACON_BLOCK_ROOT = "SYNC_COMMITTEE_ERROR_UNKNOWN_BEACON_BLOCK_ROOT", - SYNC_COMMITTEE_ALREADY_KNOWN = "SYNC_COMMITTEE_ERROR_SYNC_COMMITTEE_ALREADY_KNOWN", + SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN = "SYNC_COMMITTEE_ERROR_SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN", + SYNC_COMMITTEE_PARTICIPANTS_ALREADY_KNOWN = "SYNC_COMMITTEE_ERROR_SYNC_COMMITTEE_PARTICIPANTS_ALREADY_KNOWN", VALIDATOR_NOT_IN_SYNC_COMMITTEE = "SYNC_COMMITTEE_ERROR_VALIDATOR_NOT_IN_SYNC_COMMITTEE", INVALID_SIGNATURE = "SYNC_COMMITTEE_INVALID_SIGNATURE", INVALID_SUBCOMMITTEE_INDEX = "SYNC_COMMITTEE_INVALID_SUBCOMMITTEE_INDEX", @@ -15,7 +16,8 @@ export enum SyncCommitteeErrorCode { export type SyncCommitteeErrorType = | {code: SyncCommitteeErrorCode.NOT_CURRENT_SLOT; slot: Slot; currentSlot: Slot} | {code: SyncCommitteeErrorCode.UNKNOWN_BEACON_BLOCK_ROOT; beaconBlockRoot: Uint8Array} - | {code: SyncCommitteeErrorCode.SYNC_COMMITTEE_ALREADY_KNOWN} + | {code: SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN} + | {code: SyncCommitteeErrorCode.SYNC_COMMITTEE_PARTICIPANTS_ALREADY_KNOWN} | {code: SyncCommitteeErrorCode.VALIDATOR_NOT_IN_SYNC_COMMITTEE; validatorIndex: ValidatorIndex} | {code: SyncCommitteeErrorCode.INVALID_SIGNATURE} | {code: SyncCommitteeErrorCode.INVALID_SUBCOMMITTEE_INDEX; subcommitteeIndex: number} diff --git a/packages/lodestar/src/chain/eventHandlers.ts b/packages/lodestar/src/chain/eventHandlers.ts index 19610347fb37..a16c32e66cf9 100644 --- a/packages/lodestar/src/chain/eventHandlers.ts +++ b/packages/lodestar/src/chain/eventHandlers.ts @@ -96,6 +96,7 @@ export async function onClockSlot(this: BeaconChain, slot: Slot): Promise export function onClockEpoch(this: BeaconChain, currentEpoch: Epoch): void { this.seenAttesters.prune(currentEpoch); this.seenAggregators.prune(currentEpoch); + this.seenAggregatedAttestations.prune(currentEpoch); } export function onForkVersion(this: BeaconChain, version: Version): void { diff --git a/packages/lodestar/src/chain/interface.ts b/packages/lodestar/src/chain/interface.ts index e0126065a578..4ff772d2fdee 100644 --- a/packages/lodestar/src/chain/interface.ts +++ b/packages/lodestar/src/chain/interface.ts @@ -22,6 +22,7 @@ import {LightClientServer} from "./lightClient"; import {AggregatedAttestationPool} from "./opPools/aggregatedAttestationPool"; import {PartiallyVerifiedBlockFlags} from "./blocks/types"; import {ReprocessController} from "./reprocess"; +import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof"; export type Eth2Context = { activeValidatorCount: number; @@ -64,6 +65,7 @@ export interface IBeaconChain { // Gossip seen cache readonly seenAttesters: SeenAttesters; readonly seenAggregators: SeenAggregators; + readonly seenAggregatedAttestations: SeenAggregatedAttestations; readonly seenBlockProposers: SeenBlockProposers; readonly seenSyncCommitteeMessages: SeenSyncCommitteeMessages; readonly seenContributionAndProof: SeenContributionAndProof; diff --git a/packages/lodestar/src/chain/seenCache/seenAggregateAndProof.ts b/packages/lodestar/src/chain/seenCache/seenAggregateAndProof.ts new file mode 100644 index 000000000000..826451313d59 --- /dev/null +++ b/packages/lodestar/src/chain/seenCache/seenAggregateAndProof.ts @@ -0,0 +1,91 @@ +import {Epoch, RootHex} from "@chainsafe/lodestar-types"; +import {BitArray} from "@chainsafe/ssz"; +import {IMetrics} from "../../metrics"; +import {isSuperSetOrEqual} from "../../util/bitArray"; +import {MapDef} from "../../util/map"; + +/** + * With this gossip validation condition: [IGNORE] aggregate.data.slot is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) + * Since ATTESTATION_PROPAGATION_SLOT_RANGE is 32, we keep seen AggregateAndProof in the last 2 epochs. + */ +const MAX_EPOCHS_IN_CACHE = 2; + +export type AggregationInfo = { + aggregationBits: BitArray; + trueBitCount: number; +}; + +/** + * Although there are up to TARGET_AGGREGATORS_PER_COMMITTEE (16 for mainnet) AggregateAndProof messages per slot, + * they tend to have the same aggregate attestation, or one attestation is non-strict superset of another, + * the gossipsub messages-ids are different because they are really different SignedAggregateAndProof object. + * This is used to address the following spec in p2p-interface gossipsub: + * _[IGNORE]_ A valid aggregate attestation defined by `hash_tree_root(aggregate.data)` whose `aggregation_bits` is a + * non-strict superset has _not_ already been seen. + * + * We have AggregatedAttestationPool op pool, however aggregated attestations are not added to that place while this does. + */ +export class SeenAggregatedAttestations { + /** + * Array of AttestingIndices by same attestation data root by epoch. + * Note that there are at most TARGET_AGGREGATORS_PER_COMMITTEE (16) per attestation data. + * */ + private readonly aggregateRootsByEpoch = new MapDef>( + () => new MapDef(() => []) + ); + private lowestPermissibleEpoch: Epoch = 0; + + constructor(private readonly metrics: IMetrics | null) {} + + isKnown(targetEpoch: Epoch, attDataRoot: RootHex, aggregationBits: BitArray): boolean { + const seenAggregationInfoArr = this.aggregateRootsByEpoch.getOrDefault(targetEpoch).getOrDefault(attDataRoot); + this.metrics?.seenCache.aggregatedAttestations.isKnownCalls.inc(); + + for (let i = 0; i < seenAggregationInfoArr.length; i++) { + if (isSuperSetOrEqual(seenAggregationInfoArr[i].aggregationBits, aggregationBits)) { + this.metrics?.seenCache.aggregatedAttestations.superSetCheckTotal.observe(i + 1); + this.metrics?.seenCache.aggregatedAttestations.isKnownHits.inc(); + return true; + } + } + + this.metrics?.seenCache.aggregatedAttestations.superSetCheckTotal.observe(seenAggregationInfoArr.length); + return false; + } + + add(targetEpoch: Epoch, attDataRoot: RootHex, newItem: AggregationInfo, checkIsKnown: boolean): void { + const {aggregationBits} = newItem; + if (checkIsKnown && this.isKnown(targetEpoch, attDataRoot, aggregationBits)) { + return; + } + + const seenAggregationInfoArr = this.aggregateRootsByEpoch.getOrDefault(targetEpoch).getOrDefault(attDataRoot); + insertDesc(seenAggregationInfoArr, newItem); + } + + prune(currentEpoch: Epoch): void { + this.lowestPermissibleEpoch = Math.max(currentEpoch - MAX_EPOCHS_IN_CACHE, 0); + for (const epoch of this.aggregateRootsByEpoch.keys()) { + if (epoch < this.lowestPermissibleEpoch) { + this.aggregateRootsByEpoch.delete(epoch); + } + } + } +} + +/** + * Make sure seenAggregationInfoArr is always in desc order based on trueBitCount so that isKnown can be faster + */ +export function insertDesc(seenAggregationInfoArr: AggregationInfo[], newItem: AggregationInfo): void { + const {trueBitCount} = newItem; + let found = false; + for (let i = 0; i < seenAggregationInfoArr.length; i++) { + if (trueBitCount >= seenAggregationInfoArr[i].trueBitCount) { + seenAggregationInfoArr.splice(i, 0, newItem); + found = true; + break; + } + } + + if (!found) seenAggregationInfoArr.push(newItem); +} diff --git a/packages/lodestar/src/chain/seenCache/seenCommitteeContribution.ts b/packages/lodestar/src/chain/seenCache/seenCommitteeContribution.ts index 6fa9142fb889..be79c385b382 100644 --- a/packages/lodestar/src/chain/seenCache/seenCommitteeContribution.ts +++ b/packages/lodestar/src/chain/seenCache/seenCommitteeContribution.ts @@ -1,5 +1,10 @@ import {Slot, ValidatorIndex} from "@chainsafe/lodestar-types"; +import {ContributionAndProof, SyncCommitteeContribution} from "@chainsafe/lodestar-types/altair"; +import {toHexString} from "@chainsafe/ssz"; +import {IMetrics} from "../../metrics"; +import {isSuperSetOrEqual} from "../../util/bitArray"; import {MapDef} from "../../util/map"; +import {AggregationInfo, insertDesc} from "./seenAggregateAndProof"; /** * SyncCommittee aggregates are only useful for the next block they have signed. @@ -9,38 +14,92 @@ const MAX_SLOTS_IN_CACHE = 8; /** AggregatorSubnetKey = `aggregatorIndex + subcommitteeIndex` */ type AggregatorSubnetKey = string; +/** ContributionDataKey = `slot + beacon_block_root + subcommittee_index */ +type ContributionDataKey = string; + /** * Cache SyncCommitteeContribution and seen ContributionAndProof. * This is used for SignedContributionAndProof validation and block factory. * This stays in-memory and should be pruned per slot. */ export class SeenContributionAndProof { - private readonly seenCacheBySlot = new MapDef>(() => new Set()); + private readonly seenAggregatorBySlot = new MapDef>( + () => new Set() + ); + + private readonly seenContributionBySlot = new MapDef>( + () => new MapDef(() => []) + ); + + constructor(private readonly metrics: IMetrics | null) {} + + /** + * _[IGNORE]_ A valid sync committee contribution with equal `slot`, `beacon_block_root` and `subcommittee_index` whose + * `aggregation_bits` is non-strict superset has _not_ already been seen. + */ + participantsKnown(contribution: SyncCommitteeContribution): boolean { + const {aggregationBits, slot} = contribution; + const contributionMap = this.seenContributionBySlot.getOrDefault(slot); + const seenAggregationInfoArr = contributionMap.getOrDefault(toContributionDataKey(contribution)); + this.metrics?.seenCache.committeeContributions.isKnownCalls.inc(); + // seenAttestingIndicesArr is sorted by trueBitCount desc + + for (let i = 0; i < seenAggregationInfoArr.length; i++) { + if (isSuperSetOrEqual(seenAggregationInfoArr[i].aggregationBits, aggregationBits)) { + this.metrics?.seenCache.committeeContributions.isKnownHits.inc(); + this.metrics?.seenCache.committeeContributions.superSetCheckTotal.observe(i + 1); + return true; + } + } + + this.metrics?.seenCache.committeeContributions.superSetCheckTotal.observe(seenAggregationInfoArr.length); + return false; + } /** * Gossip validation requires to check: * The sync committee contribution is the first valid contribution received for the aggregator with index * contribution_and_proof.aggregator_index for the slot contribution.slot and subcommittee index contribution.subcommittee_index. */ - isKnown(slot: Slot, subcommitteeIndex: number, aggregatorIndex: ValidatorIndex): boolean { - return this.seenCacheBySlot.get(slot)?.has(seenCacheKey(subcommitteeIndex, aggregatorIndex)) === true; + isAggregatorKnown(slot: Slot, subcommitteeIndex: number, aggregatorIndex: ValidatorIndex): boolean { + return this.seenAggregatorBySlot.get(slot)?.has(seenAggregatorKey(subcommitteeIndex, aggregatorIndex)) === true; } /** Register item as seen in the cache */ - add(slot: Slot, subcommitteeIndex: number, aggregatorIndex: ValidatorIndex): void { - this.seenCacheBySlot.getOrDefault(slot).add(seenCacheKey(subcommitteeIndex, aggregatorIndex)); + add(contributionAndProof: ContributionAndProof, trueBitCount: number): void { + const {contribution, aggregatorIndex} = contributionAndProof; + const {subcommitteeIndex, slot, aggregationBits} = contribution; + + // add to seenAggregatorBySlot + this.seenAggregatorBySlot.getOrDefault(slot).add(seenAggregatorKey(subcommitteeIndex, aggregatorIndex)); + + // add to seenContributionBySlot + const contributionMap = this.seenContributionBySlot.getOrDefault(slot); + const seenAggregationInfoArr = contributionMap.getOrDefault(toContributionDataKey(contribution)); + insertDesc(seenAggregationInfoArr, {aggregationBits, trueBitCount}); } /** Prune per head slot */ prune(headSlot: Slot): void { - for (const slot of this.seenCacheBySlot.keys()) { + for (const slot of this.seenAggregatorBySlot.keys()) { if (slot < headSlot - MAX_SLOTS_IN_CACHE) { - this.seenCacheBySlot.delete(slot); + this.seenAggregatorBySlot.delete(slot); + } + } + + for (const slot of this.seenContributionBySlot.keys()) { + if (slot < headSlot - MAX_SLOTS_IN_CACHE) { + this.seenContributionBySlot.delete(slot); } } } } -function seenCacheKey(subcommitteeIndex: number, aggregatorIndex: ValidatorIndex): AggregatorSubnetKey { +function seenAggregatorKey(subcommitteeIndex: number, aggregatorIndex: ValidatorIndex): AggregatorSubnetKey { return `${subcommitteeIndex}-${aggregatorIndex}`; } + +function toContributionDataKey(contribution: SyncCommitteeContribution): ContributionDataKey { + const {slot, beaconBlockRoot, subcommitteeIndex} = contribution; + return `${slot} - ${toHexString(beaconBlockRoot)} - ${subcommitteeIndex}`; +} diff --git a/packages/lodestar/src/chain/validation/aggregateAndProof.ts b/packages/lodestar/src/chain/validation/aggregateAndProof.ts index a44f0d1e8109..c0f8abe03ac3 100644 --- a/packages/lodestar/src/chain/validation/aggregateAndProof.ts +++ b/packages/lodestar/src/chain/validation/aggregateAndProof.ts @@ -1,4 +1,5 @@ -import {ValidatorIndex} from "@chainsafe/lodestar-types"; +import {toHexString} from "@chainsafe/ssz"; +import {ssz, ValidatorIndex} from "@chainsafe/lodestar-types"; import { phase0, allForks, @@ -24,7 +25,9 @@ export async function validateGossipAggregateAndProof( const aggregateAndProof = signedAggregateAndProof.message; const aggregate = aggregateAndProof.aggregate; + const {aggregationBits} = aggregate; const attData = aggregate.data; + const attDataRoot = toHexString(ssz.phase0.AttestationData.hashTreeRoot(attData)); const attSlot = attData.slot; const attEpoch = computeEpochAtSlot(attSlot); const attTarget = attData.target; @@ -51,6 +54,16 @@ export async function validateGossipAggregateAndProof( }); } + // _[IGNORE]_ A valid aggregate attestation defined by `hash_tree_root(aggregate.data)` whose `aggregation_bits` + // is a non-strict superset has _not_ already been seen. + if (chain.seenAggregatedAttestations.isKnown(targetEpoch, attDataRoot, aggregationBits)) { + throw new AttestationError(GossipAction.IGNORE, { + code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN, + targetEpoch, + aggregateRoot: attDataRoot, + }); + } + // [IGNORE] The block being voted for (attestation.data.beacon_block_root) has been seen (via both gossip // and non-gossip sources) (a client MAY queue attestations for processing once block is retrieved). const attHeadBlock = verifyHeadBlockAndTargetRoot(chain, attData.beaconBlockRoot, attTarget.root, attEpoch); @@ -122,6 +135,12 @@ export async function validateGossipAggregateAndProof( } chain.seenAggregators.add(targetEpoch, aggregatorIndex); + chain.seenAggregatedAttestations.add( + targetEpoch, + attDataRoot, + {aggregationBits, trueBitCount: attestingIndices.length}, + false + ); return {indexedAttestation, committeeIndices}; } diff --git a/packages/lodestar/src/chain/validation/syncCommittee.ts b/packages/lodestar/src/chain/validation/syncCommittee.ts index 3ff11490cbb7..1e1f33cd74dd 100644 --- a/packages/lodestar/src/chain/validation/syncCommittee.ts +++ b/packages/lodestar/src/chain/validation/syncCommittee.ts @@ -32,7 +32,7 @@ export async function validateGossipSyncCommittee( // by sync_committee_signature.validator_index. if (chain.seenSyncCommitteeMessages.isKnown(slot, subnet, validatorIndex)) { throw new SyncCommitteeError(GossipAction.IGNORE, { - code: SyncCommitteeErrorCode.SYNC_COMMITTEE_ALREADY_KNOWN, + code: SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN, }); } diff --git a/packages/lodestar/src/chain/validation/syncCommitteeContributionAndProof.ts b/packages/lodestar/src/chain/validation/syncCommitteeContributionAndProof.ts index 9a160a017618..3527d4e086fa 100644 --- a/packages/lodestar/src/chain/validation/syncCommitteeContributionAndProof.ts +++ b/packages/lodestar/src/chain/validation/syncCommitteeContributionAndProof.ts @@ -35,11 +35,19 @@ export async function validateSyncCommitteeGossipContributionAndProof( // get_sync_subcommittee_pubkeys(state, contribution.subcommittee_index). // > Checked in validateGossipSyncCommitteeExceptSig() + // _[IGNORE]_ A valid sync committee contribution with equal `slot`, `beacon_block_root` and `subcommittee_index` whose + // `aggregation_bits` is non-strict superset has _not_ already been seen. + if (chain.seenContributionAndProof.participantsKnown(contribution)) { + throw new SyncCommitteeError(GossipAction.IGNORE, { + code: SyncCommitteeErrorCode.SYNC_COMMITTEE_PARTICIPANTS_ALREADY_KNOWN, + }); + } + // [IGNORE] The sync committee contribution is the first valid contribution received for the aggregator with index // contribution_and_proof.aggregator_index for the slot contribution.slot and subcommittee index contribution.subcommittee_index. - if (chain.seenContributionAndProof.isKnown(slot, subcommitteeIndex, aggregatorIndex)) { + if (chain.seenContributionAndProof.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)) { throw new SyncCommitteeError(GossipAction.IGNORE, { - code: SyncCommitteeErrorCode.SYNC_COMMITTEE_ALREADY_KNOWN, + code: SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN, }); } @@ -85,7 +93,7 @@ export async function validateSyncCommitteeGossipContributionAndProof( } // no need to add to seenSyncCommittteeContributionCache here, gossip handler will do that - chain.seenContributionAndProof.add(slot, subcommitteeIndex, aggregatorIndex); + chain.seenContributionAndProof.add(contributionAndProof, syncCommitteeIndices.length); return {syncCommitteeParticipants: syncCommitteeIndices.length}; } diff --git a/packages/lodestar/src/constants/network.ts b/packages/lodestar/src/constants/network.ts index 29bf444a9eaf..47af0d96a941 100644 --- a/packages/lodestar/src/constants/network.ts +++ b/packages/lodestar/src/constants/network.ts @@ -10,7 +10,7 @@ /** * The maximum number of slots during which an attestation can be propagated. */ -export const ATTESTATION_PROPAGATION_SLOT_RANGE = 23; +export const ATTESTATION_PROPAGATION_SLOT_RANGE = 32; // Request/Response constants diff --git a/packages/lodestar/src/metrics/metrics/lodestar.ts b/packages/lodestar/src/metrics/metrics/lodestar.ts index 0eb24d599af5..49b84dc6c212 100644 --- a/packages/lodestar/src/metrics/metrics/lodestar.ts +++ b/packages/lodestar/src/metrics/metrics/lodestar.ts @@ -809,6 +809,39 @@ export function createLodestarMetrics( }), }, + seenCache: { + aggregatedAttestations: { + superSetCheckTotal: register.histogram({ + name: "lodestar_seen_cache_aggregated_attestations_super_set_check_total", + help: "Number of times to call isNonStrictSuperSet in SeenAggregatedAttestations", + buckets: [1, 4, 10], + }), + isKnownCalls: register.gauge({ + name: "lodestar_seen_cache_aggregated_attestations_is_known_call_total", + help: "Total times calling SeenAggregatedAttestations.isKnown", + }), + isKnownHits: register.gauge({ + name: "lodestar_seen_cache_aggregated_attestations_is_known_hit_total", + help: "Total times SeenAggregatedAttestations.isKnown returning true", + }), + }, + committeeContributions: { + superSetCheckTotal: register.histogram({ + name: "lodestar_seen_cache_committee_contributions_super_set_check_total", + help: "Number of times to call isNonStrictSuperSet in SeenContributionAndProof", + buckets: [1, 4, 10], + }), + isKnownCalls: register.gauge({ + name: "lodestar_seen_cache_committee_contributions_is_known_call_total", + help: "Total times calling SeenContributionAndProof.isKnown", + }), + isKnownHits: register.gauge({ + name: "lodestar_seen_cache_committee_contributions_is_known_hit_total", + help: "Total times SeenContributionAndProof.isKnown returning true", + }), + }, + }, + regenFnCallTotal: register.gauge<"entrypoint" | "caller">({ name: "lodestar_regen_fn_call_total", help: "Total number of calls for regen functions", diff --git a/packages/lodestar/src/util/bitArray.ts b/packages/lodestar/src/util/bitArray.ts index ab56c2b6b060..364110fe958f 100644 --- a/packages/lodestar/src/util/bitArray.ts +++ b/packages/lodestar/src/util/bitArray.ts @@ -1,3 +1,5 @@ +import {BitArray} from "@chainsafe/ssz"; + export enum IntersectResult { Equal, /** All elements in set B are in set A */ @@ -72,3 +74,11 @@ export function intersectUint8Arrays(aUA: Uint8Array, bUA: Uint8Array): Intersec // intersect = any other condition else return IntersectResult.Intersect; } + +/** + * Check if first BitArray is equal to or superset of the second + */ +export function isSuperSetOrEqual(superSet: BitArray, toCheck: BitArray): boolean { + const intersectionResult = intersectUint8Arrays(superSet.uint8Array, toCheck.uint8Array); + return intersectionResult === IntersectResult.Superset || intersectionResult === IntersectResult.Equal; +} diff --git a/packages/lodestar/test/perf/chain/seenCache/seenAggregateAndProof.test.ts b/packages/lodestar/test/perf/chain/seenCache/seenAggregateAndProof.test.ts new file mode 100644 index 000000000000..9c3004513764 --- /dev/null +++ b/packages/lodestar/test/perf/chain/seenCache/seenAggregateAndProof.test.ts @@ -0,0 +1,51 @@ +import {TARGET_AGGREGATORS_PER_COMMITTEE} from "@chainsafe/lodestar-params"; +import {BitArray} from "@chainsafe/ssz"; +import {itBench} from "@dapplion/benchmark"; +import {SeenAggregatedAttestations} from "../../../../src/chain/seenCache/seenAggregateAndProof"; + +describe("SeenAggregatedAttestations perf test", function () { + const targetEpoch = 2022; + const attDataRoot = "0x55e1a1cce2aeb66f85b2285b8cb7aa55dfb67148b5e0067f0692b61ddbd2824b"; + const fullByte = 0b11111111; + // as of May 2022, there are ~24*8 attesters per committee (slot + index) + const numAttestersInByte = 24; + const seedBits = new Uint8Array(Array.from({length: numAttestersInByte}, () => fullByte)); + const toAggregationBitsSingleFalse = (i: number): BitArray => { + const bits = new Uint8Array(seedBits.buffer); + const aggregationBits = new BitArray(bits, numAttestersInByte * 8); + aggregationBits.set(i, false); + return aggregationBits; + }; + + const testCases: {id: string; aggregationBits: BitArray}[] = [ + {id: "isKnown best case - 1 super set check", aggregationBits: toAggregationBitsSingleFalse(0)}, + // as monitored in metric, there are 2 set check check in average + {id: "isKnown normal case - 2 super set checks", aggregationBits: toAggregationBitsSingleFalse(1)}, + { + id: "isKnown worse case - 16 super set checks", + aggregationBits: toAggregationBitsSingleFalse(TARGET_AGGREGATORS_PER_COMMITTEE - 1), + }, + ]; + + for (const {id, aggregationBits} of testCases) { + itBench({ + id, + beforeEach: () => { + const seenCache = new SeenAggregatedAttestations(null); + // worse case scenario is we have TARGET_AGGREGATORS_PER_COMMITTEE (16) per attestation data + for (let i = 0; i < TARGET_AGGREGATORS_PER_COMMITTEE; i++) { + const aggregationInfo = { + aggregationBits: toAggregationBitsSingleFalse(i), + trueBitCount: numAttestersInByte * 8 - 1, + }; + seenCache.add(targetEpoch, attDataRoot, aggregationInfo, false); + } + + return seenCache; + }, + fn: (seenCache) => { + seenCache.isKnown(targetEpoch, attDataRoot, aggregationBits); + }, + }); + } +}); diff --git a/packages/lodestar/test/perf/chain/validation/aggregateAndProof.test.ts b/packages/lodestar/test/perf/chain/validation/aggregateAndProof.test.ts index 159bc8689b48..f5acd2df69f4 100644 --- a/packages/lodestar/test/perf/chain/validation/aggregateAndProof.test.ts +++ b/packages/lodestar/test/perf/chain/validation/aggregateAndProof.test.ts @@ -17,7 +17,10 @@ describe("validate gossip signedAggregateAndProof", () => { for (const [id, agg] of Object.entries({struct: aggStruct})) { itBench({ id: `validate gossip signedAggregateAndProof - ${id}`, - beforeEach: () => chain.seenAggregators["validatorIndexesByEpoch"].clear(), + beforeEach: () => { + chain.seenAggregators["validatorIndexesByEpoch"].clear(); + chain.seenAggregatedAttestations["aggregateRootsByEpoch"].clear(); + }, fn: async () => { await validateGossipAggregateAndProof(chain, agg); }, diff --git a/packages/lodestar/test/unit/chain/seenCache/aggregateAndProof.test.ts b/packages/lodestar/test/unit/chain/seenCache/aggregateAndProof.test.ts new file mode 100644 index 000000000000..f37faec26c37 --- /dev/null +++ b/packages/lodestar/test/unit/chain/seenCache/aggregateAndProof.test.ts @@ -0,0 +1,108 @@ +import {BitArray} from "@chainsafe/ssz"; +import {expect} from "chai"; +import { + AggregationInfo, + insertDesc, + SeenAggregatedAttestations, +} from "../../../../src/chain/seenCache/seenAggregateAndProof"; + +describe("SeenAggregatedAttestations.isKnown", function () { + const testCases: { + id: string; + seenAttestingBits: number[]; + checkAttestingBits: {bits: number[]; isKnown: boolean}[]; + }[] = [ + // Note: attestationsToAdd MUST intersect in order to not be aggregated and distort the results + { + id: "All have attested", + seenAttestingBits: [0b11111111], + checkAttestingBits: [ + {bits: [0b11111110], isKnown: true}, + {bits: [0b00000011], isKnown: true}, + ], + }, + { + id: "Some have attested", + seenAttestingBits: [0b11110001], // equals to indexes [ 0, 4, 5, 6, 7 ] + checkAttestingBits: [ + {bits: [0b11111110], isKnown: false}, + {bits: [0b00000011], isKnown: false}, + {bits: [0b11010001], isKnown: true}, + ], + }, + { + id: "Non have attested", + seenAttestingBits: [0b00000000], + checkAttestingBits: [ + {bits: [0b11111110], isKnown: false}, + {bits: [0b00000011], isKnown: false}, + ], + }, + ]; + + const targetEpoch = 10; + const attDataRoot = "0x"; + + for (const {id, seenAttestingBits, checkAttestingBits} of testCases) { + it(id, () => { + const cache = new SeenAggregatedAttestations(null); + const aggregationBits = new BitArray(new Uint8Array(seenAttestingBits), 8); + cache.add( + targetEpoch, + attDataRoot, + {aggregationBits, trueBitCount: aggregationBits.getTrueBitIndexes().length}, + false + ); + for (const {bits, isKnown} of checkAttestingBits) { + // expect(cache.participantsKnown(subsetContribution)).to.equal(isKnown); + const toCheckAggBits = new BitArray(new Uint8Array(bits), 8); + expect(cache.isKnown(targetEpoch, attDataRoot, toCheckAggBits)).to.be.equal(isKnown); + } + }); + } +}); + +describe("insertDesc", function () { + const testCases: { + id: string; + arr: number[][]; + bits: number[]; + result: number[][]; + }[] = [ + { + id: "Insert first", + arr: [[0b11110001], [0b11100001]], + bits: [0b11110001], + result: [[0b11110001], [0b11110001], [0b11100001]], + }, + { + id: "Insert second", + arr: [[0b11110001], [0b00000001]], + bits: [0b00010001], + result: [[0b11110001], [0b00010001], [0b00000001]], + }, + { + id: "Insert last", + arr: [[0b11110001], [0b00000011]], + bits: [0b00000001], + result: [[0b11110001], [0b00000011], [0b00000001]], + }, + ]; + + const toAggregationBits = (bits: number[]): AggregationInfo => { + const aggregationBits = new BitArray(new Uint8Array(bits), 8); + return { + aggregationBits, + trueBitCount: aggregationBits.getTrueBitIndexes().length, + }; + }; + + for (const {id, arr, bits, result} of testCases) { + it(id, () => { + const seenAggregationInfoArr = arr.map(toAggregationBits); + + insertDesc(seenAggregationInfoArr, toAggregationBits(bits)); + expect(seenAggregationInfoArr).to.be.deep.equal(result.map(toAggregationBits)); + }); + } +}); diff --git a/packages/lodestar/test/unit/chain/seenCache/syncCommittee.test.ts b/packages/lodestar/test/unit/chain/seenCache/syncCommittee.test.ts index 3e19c5efcba7..8653fdfa9eec 100644 --- a/packages/lodestar/test/unit/chain/seenCache/syncCommittee.test.ts +++ b/packages/lodestar/test/unit/chain/seenCache/syncCommittee.test.ts @@ -1,5 +1,7 @@ +import {BitArray} from "@chainsafe/ssz"; import {expect} from "chai"; import {SeenSyncCommitteeMessages, SeenContributionAndProof} from "../../../../src/chain/seenCache"; +import {generateContributionAndProof} from "../../../utils/contributionAndProof"; const NUM_SLOTS_IN_CACHE = 3; @@ -36,31 +38,116 @@ describe("chain / seenCache / SeenSyncCommittee caches", function () { describe("SeenContributionAndProof", () => { const slot = 10; - const subnet = 2; + const subcommitteeIndex = 2; const aggregatorIndex = 100; it("should find a sync committee based on same slot and validator index", () => { - const cache = new SeenContributionAndProof(); + const cache = new SeenContributionAndProof(null); - expect(cache.isKnown(slot, subnet, aggregatorIndex)).to.equal(false, "Should not know before adding"); - cache.add(slot, subnet, aggregatorIndex); - expect(cache.isKnown(slot, subnet, aggregatorIndex)).to.equal(true, "Should know before adding"); + expect(cache.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)).to.equal( + false, + "Should not know before adding" + ); + cache.add(generateContributionAndProof({aggregatorIndex, contribution: {slot, subcommitteeIndex}}), 0); + expect(cache.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)).to.equal( + true, + "Should know before adding" + ); - expect(cache.isKnown(slot + 1, subnet, aggregatorIndex)).to.equal(false, "Should not know a diff slot"); - expect(cache.isKnown(slot, subnet + 1, aggregatorIndex)).to.equal(false, "Should not know a diff subnet"); - expect(cache.isKnown(slot, subnet, aggregatorIndex + 1)).to.equal(false, "Should not know a diff index"); + expect(cache.isAggregatorKnown(slot + 1, subcommitteeIndex, aggregatorIndex)).to.equal( + false, + "Should not know a diff slot" + ); + expect(cache.isAggregatorKnown(slot, subcommitteeIndex + 1, aggregatorIndex)).to.equal( + false, + "Should not know a diff subnet" + ); + expect(cache.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex + 1)).to.equal( + false, + "Should not know a diff index" + ); }); it("should prune", () => { - const cache = new SeenContributionAndProof(); + const cache = new SeenContributionAndProof(null); + const contributionAndProof = generateContributionAndProof({ + aggregatorIndex, + contribution: {slot, subcommitteeIndex}, + }); for (let i = 0; i < NUM_SLOTS_IN_CACHE; i++) { - cache.add(slot, subnet, aggregatorIndex); + cache.add(contributionAndProof, 0); } - expect(cache.isKnown(slot, subnet, aggregatorIndex)).to.equal(true, "Should know before prune"); + expect(cache.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)).to.equal( + true, + "Should know before prune" + ); + expect(cache.participantsKnown(contributionAndProof.contribution)).to.equal(true, "Should know participants"); + cache.prune(99); - expect(cache.isKnown(slot, subnet, aggregatorIndex)).to.equal(false, "Should not know after prune"); + + expect(cache.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)).to.equal( + false, + "Should not know after prune" + ); + expect(cache.participantsKnown(contributionAndProof.contribution)).to.equal( + false, + "Should not know participants" + ); }); + + const testCases: { + id: string; + seenAttestingBits: number[]; + checkAttestingBits: {bits: number[]; isKnown: boolean}[]; + }[] = [ + // Note: attestationsToAdd MUST intersect in order to not be aggregated and distort the results + { + id: "SeenContributionAndProof.participantsKnown - All have attested", + seenAttestingBits: [0b11111111], + checkAttestingBits: [ + {bits: [0b11111110], isKnown: true}, + {bits: [0b00000011], isKnown: true}, + ], + }, + { + id: "SeenContributionAndProof.participantsKnown - Some have attested", + seenAttestingBits: [0b11110001], // equals to indexes [ 0, 4, 5, 6, 7 ] + checkAttestingBits: [ + {bits: [0b11111110], isKnown: false}, + {bits: [0b00000011], isKnown: false}, + {bits: [0b11010001], isKnown: true}, + ], + }, + { + id: "SeenContributionAndProof.participantsKnown - Non have attested", + seenAttestingBits: [0b00000000], + checkAttestingBits: [ + {bits: [0b11111110], isKnown: false}, + {bits: [0b00000011], isKnown: false}, + ], + }, + ]; + + for (const {id, seenAttestingBits, checkAttestingBits} of testCases) { + it(id, () => { + const cache = new SeenContributionAndProof(null); + const aggregationBits = new BitArray(new Uint8Array(seenAttestingBits), 8); + const contributionAndProof = generateContributionAndProof({ + aggregatorIndex, + contribution: {slot, subcommitteeIndex, aggregationBits}, + }); + cache.add(contributionAndProof, aggregationBits.getTrueBitIndexes().length); + + for (const {bits, isKnown} of checkAttestingBits) { + const subsetContribution = { + ...contributionAndProof.contribution, + aggregationBits: new BitArray(new Uint8Array(bits), 8), + }; + expect(cache.participantsKnown(subsetContribution)).to.equal(isKnown); + } + }); + } }); }); diff --git a/packages/lodestar/test/unit/chain/validation/aggregateAndProof.test.ts b/packages/lodestar/test/unit/chain/validation/aggregateAndProof.test.ts index 9559ac9f6004..f7705fed9a3a 100644 --- a/packages/lodestar/test/unit/chain/validation/aggregateAndProof.test.ts +++ b/packages/lodestar/test/unit/chain/validation/aggregateAndProof.test.ts @@ -1,5 +1,6 @@ +import {toHexString} from "@chainsafe/ssz"; import {SLOTS_PER_EPOCH} from "@chainsafe/lodestar-params"; -import {phase0} from "@chainsafe/lodestar-types"; +import {phase0, ssz} from "@chainsafe/lodestar-types"; import {IBeaconChain} from "../../../../src/chain"; import {AttestationErrorCode} from "../../../../src/chain/errors"; import {validateGossipAggregateAndProof} from "../../../../src/chain/validation"; @@ -64,6 +65,21 @@ describe("chain / validation / aggregateAndProof", () => { await expectError(chain, signedAggregateAndProof, AttestationErrorCode.FUTURE_SLOT); }); + it("ATTESTING_INDICES_ALREADY_KNOWN", async () => { + const {chain, signedAggregateAndProof} = getValidData(); + const {aggregationBits} = signedAggregateAndProof.message.aggregate; + const attData = signedAggregateAndProof.message.aggregate.data; + // Register attester as already seen + chain.seenAggregatedAttestations.add( + attData.target.epoch, + toHexString(ssz.phase0.AttestationData.hashTreeRoot(attData)), + {aggregationBits, trueBitCount: aggregationBits.getTrueBitIndexes().length}, + false + ); + + await expectError(chain, signedAggregateAndProof, AttestationErrorCode.ATTESTERS_ALREADY_KNOWN); + }); + it("AGGREGATOR_ALREADY_KNOWN", async () => { const {chain, signedAggregateAndProof} = getValidData(); // Register attester as already seen diff --git a/packages/lodestar/test/unit/chain/validation/contributionAndProof.test.ts b/packages/lodestar/test/unit/chain/validation/contributionAndProof.test.ts index 2b42ff61f5b0..4f79e5ca255a 100644 --- a/packages/lodestar/test/unit/chain/validation/contributionAndProof.test.ts +++ b/packages/lodestar/test/unit/chain/validation/contributionAndProof.test.ts @@ -39,7 +39,7 @@ describe("Sync Committee Contribution And Proof validation", function () { chain = sandbox.createStubInstance(BeaconChain); (chain as { seenContributionAndProof: SeenContributionAndProof; - }).seenContributionAndProof = new SeenContributionAndProof(); + }).seenContributionAndProof = new SeenContributionAndProof(null); clockStub = sandbox.createStubInstance(LocalClock); chain.clock = clockStub; clockStub.isCurrentSlotGivenGossipDisparity.returns(true); @@ -73,6 +73,20 @@ describe("Sync Committee Contribution And Proof validation", function () { ); }); + it("should throw error - same contribution data with superset of aggregationBits already known", async function () { + const signedContributionAndProof = generateSignedContributionAndProof({ + contribution: {slot: currentSlot}, + aggregatorIndex, + }); + const headState = await generateCachedStateWithPubkeys({slot: currentSlot}, config, true); + chain.getHeadState.returns(headState); + chain.seenContributionAndProof.participantsKnown = () => true; + await expectRejectedWithLodestarError( + validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), + SyncCommitteeErrorCode.SYNC_COMMITTEE_PARTICIPANTS_ALREADY_KNOWN + ); + }); + it("should throw error - there is same contribution with same aggregator and index and slot", async function () { const signedContributionAndProof = generateSignedContributionAndProof({ contribution: {slot: currentSlot}, @@ -80,10 +94,10 @@ describe("Sync Committee Contribution And Proof validation", function () { }); const headState = await generateCachedStateWithPubkeys({slot: currentSlot}, config, true); chain.getHeadState.returns(headState); - chain.seenContributionAndProof.isKnown = () => true; + chain.seenContributionAndProof.isAggregatorKnown = () => true; await expectRejectedWithLodestarError( validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof), - SyncCommitteeErrorCode.SYNC_COMMITTEE_ALREADY_KNOWN + SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN ); }); diff --git a/packages/lodestar/test/unit/chain/validation/syncCommittee.test.ts b/packages/lodestar/test/unit/chain/validation/syncCommittee.test.ts index e249216cc553..635ff6ff7ddc 100644 --- a/packages/lodestar/test/unit/chain/validation/syncCommittee.test.ts +++ b/packages/lodestar/test/unit/chain/validation/syncCommittee.test.ts @@ -72,7 +72,7 @@ describe("Sync Committee Signature validation", function () { chain.seenSyncCommitteeMessages.isKnown = () => true; await expectRejectedWithLodestarError( validateGossipSyncCommittee(chain, syncCommittee, 0), - SyncCommitteeErrorCode.SYNC_COMMITTEE_ALREADY_KNOWN + SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN ); }); diff --git a/packages/lodestar/test/utils/mocks/chain/chain.ts b/packages/lodestar/test/utils/mocks/chain/chain.ts index c0eaf5c3a0e8..ca0b8a14433e 100644 --- a/packages/lodestar/test/utils/mocks/chain/chain.ts +++ b/packages/lodestar/test/utils/mocks/chain/chain.ts @@ -37,6 +37,7 @@ import {ReqRespBlockResponse} from "../../../../src/network/reqresp/types"; import {testLogger} from "../../logger"; import {ReprocessController} from "../../../../src/chain/reprocess"; import {createCachedBeaconStateTest} from "@chainsafe/lodestar-beacon-state-transition/test/utils/state"; +import {SeenAggregatedAttestations} from "../../../../src/chain/seenCache/seenAggregateAndProof"; /* eslint-disable @typescript-eslint/no-empty-function */ @@ -78,9 +79,10 @@ export class MockBeaconChain implements IBeaconChain { // Gossip seen cache readonly seenAttesters = new SeenAttesters(); readonly seenAggregators = new SeenAggregators(); + readonly seenAggregatedAttestations = new SeenAggregatedAttestations(null); readonly seenBlockProposers = new SeenBlockProposers(); readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); - readonly seenContributionAndProof = new SeenContributionAndProof(); + readonly seenContributionAndProof = new SeenContributionAndProof(null); private state: BeaconStateAllForks; private abortController: AbortController; diff --git a/packages/lodestar/test/utils/validationData/attestation.ts b/packages/lodestar/test/utils/validationData/attestation.ts index bd0668edd405..38b5d140d221 100644 --- a/packages/lodestar/test/utils/validationData/attestation.ts +++ b/packages/lodestar/test/utils/validationData/attestation.ts @@ -20,6 +20,7 @@ import {ClockStatic} from "../clock"; import {BitArray, toHexString} from "@chainsafe/ssz"; import {config} from "@chainsafe/lodestar-config/default"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {SeenAggregatedAttestations} from "../../../src/chain/seenCache/seenAggregateAndProof"; export type AttestationValidDataOpts = { currentSlot?: Slot; @@ -115,6 +116,7 @@ export function getAttestationValidData( forkChoice, regen, seenAttesters: new SeenAttesters(), + seenAggregatedAttestations: new SeenAggregatedAttestations(null), bls: new BlsSingleThreadVerifier({metrics: null}), waitForBlockOfAttestation: () => Promise.resolve(false), } as Partial) as IBeaconChain;