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 bc8c838a540..398238aa250 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -1,7 +1,7 @@ import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; -import {Epoch, ssz} from "@lodestar/types"; -import {ForkName, SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params"; +import {Attestation, Epoch, isElectraAttestation, ssz} from "@lodestar/types"; +import {ForkName, SYNC_COMMITTEE_SUBNET_SIZE, isForkPostElectra} from "@lodestar/params"; import {validateApiAttestation} from "../../../../chain/validation/index.js"; import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js"; import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js"; @@ -16,6 +16,7 @@ import { SyncCommitteeError, } from "../../../../chain/errors/index.js"; import {validateGossipFnRetryUnknownRoot} from "../../../../network/processor/gossipHandlers.js"; +import {ApiError} from "../../errors.js"; export function getBeaconPoolApi({ chain, @@ -26,7 +27,15 @@ export function getBeaconPoolApi({ return { async getPoolAttestations({slot, committeeIndex}) { // Already filtered by slot - let attestations = chain.aggregatedAttestationPool.getAll(slot); + let attestations: Attestation[] = chain.aggregatedAttestationPool.getAll(slot); + const fork = chain.config.getForkName(slot ?? chain.clock.currentSlot); + + if (isForkPostElectra(fork)) { + throw new ApiError( + 400, + `Use getPoolAttestationsV2 to retrieve pool attestations for post-electra fork=${fork}` + ); + } if (committeeIndex !== undefined) { attestations = attestations.filter((attestation) => committeeIndex === attestation.data.index); @@ -39,6 +48,11 @@ export function getBeaconPoolApi({ // Already filtered by slot let attestations = chain.aggregatedAttestationPool.getAll(slot); const fork = chain.config.getForkName(slot ?? attestations[0]?.data.slot ?? chain.clock.currentSlot); + const isPostElectra = isForkPostElectra(fork); + + attestations = attestations.filter((attestation) => + isPostElectra ? isElectraAttestation(attestation) : !isElectraAttestation(attestation) + ); if (committeeIndex !== undefined) { attestations = attestations.filter((attestation) => committeeIndex === attestation.data.index); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 298e7c6f448..dba1a307a36 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -21,6 +21,7 @@ import { ForkPreBlobs, ForkBlobs, ForkExecution, + isForkPostElectra, } from "@lodestar/params"; import {MAX_BUILDER_BOOST_FACTOR} from "@lodestar/validator"; import { @@ -1067,9 +1068,31 @@ export function getValidatorApi( }; }, - // TODO Electra: Implement getAggregatedAttestation to properly handle pre-electra - async getAggregatedAttestation() { - throw new Error("Not implemented. Use getAggregatedAttestationV2 for now."); + async getAggregatedAttestation({attestationDataRoot, slot}) { + notWhileSyncing(); + + await waitForSlot(slot); // Must never request for a future slot > currentSlot + + const dataRootHex = toHex(attestationDataRoot); + const aggregate = chain.attestationPool.getAggregate(slot, null, dataRootHex); + const fork = chain.config.getForkName(slot); + + if (isForkPostElectra(fork)) { + throw new ApiError( + 400, + `Use getAggregatedAttestationV2 to retrieve aggregated attestations for post-electra fork=${fork}` + ); + } + + if (!aggregate) { + throw new ApiError(404, `No aggregated attestation for slot=${slot}, dataRoot=${dataRootHex}`); + } + + metrics?.production.producedAggregateParticipants.observe(aggregate.aggregationBits.getTrueBitIndexes().length); + + return { + data: aggregate, + }; }, async getAggregatedAttestationV2({attestationDataRoot, slot, committeeIndex}) { diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 63a85ba4f4a..10ec114fce7 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -133,7 +133,7 @@ export class BeaconChain implements IBeaconChain { // Ops pool readonly attestationPool: AttestationPool; - readonly aggregatedAttestationPool = new AggregatedAttestationPool(); + readonly aggregatedAttestationPool: AggregatedAttestationPool; readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; readonly syncContributionAndProofPool = new SyncContributionAndProofPool(); readonly opPool = new OpPool(); @@ -226,7 +226,13 @@ export class BeaconChain implements IBeaconChain { if (!clock) clock = new Clock({config, genesisTime: this.genesisTime, signal}); const preAggregateCutOffTime = (2 / 3) * this.config.SECONDS_PER_SLOT; - this.attestationPool = new AttestationPool(clock, preAggregateCutOffTime, this.opts?.preaggregateSlotDistance); + this.attestationPool = new AttestationPool( + config, + clock, + preAggregateCutOffTime, + this.opts?.preaggregateSlotDistance + ); + this.aggregatedAttestationPool = new AggregatedAttestationPool(this.config); this.syncCommitteeMessagePool = new SyncCommitteeMessagePool( clock, preAggregateCutOffTime, diff --git a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts index 6751329a9c1..446e522e402 100644 --- a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts @@ -3,6 +3,7 @@ import {BitArray, toHexString} from "@chainsafe/ssz"; import { ForkName, ForkSeq, + isForkPostElectra, MAX_ATTESTATIONS, MAX_ATTESTATIONS_ELECTRA, MAX_COMMITTEES_PER_SLOT, @@ -30,6 +31,7 @@ import { } from "@lodestar/state-transition"; import {IForkChoice, EpochDifference} from "@lodestar/fork-choice"; import {toHex, MapDef, assert} from "@lodestar/utils"; +import {ChainForkConfig} from "@lodestar/config"; import {intersectUint8Arrays, IntersectResult} from "../../util/bitArray.js"; import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; import {InsertOutcome} from "./types.js"; @@ -101,6 +103,8 @@ export class AggregatedAttestationPool { >(() => new Map>()); private lowestPermissibleSlot = 0; + constructor(private readonly config: ChainForkConfig) {} + /** For metrics to track size of the pool */ getAttestationCount(): {attestationCount: number; attestationDataCount: number} { let attestationCount = 0; @@ -136,10 +140,20 @@ export class AggregatedAttestationPool { attestationGroupByIndex = new Map(); attestationGroupByIndexByDataHash.set(dataRootHex, attestationGroupByIndex); } - const committeeIndex = isElectraAttestation(attestation) - ? // this attestation is added to pool after validation - attestation.committeeBits.getSingleTrueBit() - : attestation.data.index; + + let committeeIndex; + + if (isForkPostElectra(this.config.getForkName(slot))) { + if (!isElectraAttestation(attestation)) { + throw Error(`Attestation should be type electra.Attestation for slot ${slot}`); + } + committeeIndex = attestation.committeeBits.getSingleTrueBit(); + } else { + if (isElectraAttestation(attestation)) { + throw Error(`Attestation should be type phase0.Attestation for slot ${slot}`); + } + committeeIndex = 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 aggregated attestation pool"); let attestationGroup = attestationGroupByIndex.get(committeeIndex); @@ -390,6 +404,10 @@ export class AggregatedAttestationPool { /** * Get all attestations optionally filtered by `attestation.data.slot` + * Note this function is not fork aware and can potentially return a mix + * of phase0.Attestations and electra.Attestations. + * Caller of this function is expected to filtered result if they desire + * a homogenous array. * @param bySlot slot to filter, `bySlot === attestation.data.slot` */ getAll(bySlot?: Slot): Attestation[] { @@ -504,8 +522,15 @@ export class MatchingDataAttestationGroup { */ getAttestationsForBlock(fork: ForkName, notSeenAttestingIndices: Set): AttestationNonParticipant[] { const attestations: AttestationNonParticipant[] = []; - const forkSeq = ForkSeq[fork]; + const isPostElectra = isForkPostElectra(fork); for (const {attestation} of this.attestations) { + if ( + (isPostElectra && !isElectraAttestation(attestation)) || + (!isPostElectra && isElectraAttestation(attestation)) + ) { + continue; + } + let notSeenAttesterCount = 0; const {aggregationBits} = attestation; for (const notSeenIndex of notSeenAttestingIndices) { @@ -514,13 +539,12 @@ export class MatchingDataAttestationGroup { } } - // if fork >= electra, should return electra-only attestations - if (notSeenAttesterCount > 0 && (forkSeq < ForkSeq.electra || isElectraAttestation(attestation))) { + if (notSeenAttesterCount > 0) { attestations.push({attestation, notSeenAttesterCount}); } } - const maxAttestation = forkSeq >= ForkSeq.electra ? MAX_ATTESTATIONS_PER_GROUP_ELECTRA : MAX_ATTESTATIONS_PER_GROUP; + const maxAttestation = isPostElectra ? MAX_ATTESTATIONS_PER_GROUP_ELECTRA : MAX_ATTESTATIONS_PER_GROUP; if (attestations.length <= maxAttestation) { return attestations; } else { diff --git a/packages/beacon-node/src/chain/opPools/attestationPool.ts b/packages/beacon-node/src/chain/opPools/attestationPool.ts index f125b8c941d..887448b1e55 100644 --- a/packages/beacon-node/src/chain/opPools/attestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/attestationPool.ts @@ -2,9 +2,11 @@ import {BitArray} from "@chainsafe/ssz"; import {Signature, aggregateSignatures} from "@chainsafe/blst"; import {Slot, RootHex, isElectraAttestation, Attestation} from "@lodestar/types"; import {MapDef, assert} from "@lodestar/utils"; +import {isForkPostElectra} from "@lodestar/params"; +import {ChainForkConfig} from "@lodestar/config"; import {IClock} from "../../util/clock.js"; import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js"; -import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; +import {isElectraAggregate, pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; /** * The number of slots that will be stored in the pool. @@ -28,14 +30,15 @@ type AggregateFastPhase0 = { signature: Signature; }; -type AggregateFastElectra = AggregateFastPhase0 & {committeeBits: BitArray}; +export type AggregateFastElectra = AggregateFastPhase0 & {committeeBits: BitArray}; -type AggregateFast = AggregateFastPhase0 | AggregateFastElectra; +export type AggregateFast = AggregateFastPhase0 | AggregateFastElectra; /** Hex string of DataRoot `TODO` */ type DataRootHex = string; -type CommitteeIndex = number; +/** CommitteeIndex must be null for pre-electra. Must not be null post-electra */ +type CommitteeIndex = number | null; /** * A pool of `Attestation` that is specially designed to store "unaggregated" attestations from @@ -68,6 +71,7 @@ export class AttestationPool { private lowestPermissibleSlot = 0; constructor( + private readonly config: ChainForkConfig, private readonly clock: IClock, private readonly cutOffSecFromSlot: number, private readonly preaggregateSlotDistance = 0 @@ -103,6 +107,7 @@ export class AttestationPool { */ add(committeeIndex: CommitteeIndex, attestation: Attestation, attDataRootHex: RootHex): InsertOutcome { const slot = attestation.data.slot; + const fork = this.config.getForkName(slot); const lowestPermissibleSlot = this.lowestPermissibleSlot; // Reject any attestations that are too old. @@ -121,8 +126,14 @@ export class AttestationPool { throw new OpPoolError({code: OpPoolErrorCode.REACHED_MAX_PER_SLOT}); } - // this should not happen because attestation should be validated before reaching this - assert.notNull(committeeIndex, "Committee index should not be null in attestation pool"); + if (isForkPostElectra(fork)) { + // Electra only: this should not happen because attestation should be validated before reaching this + assert.notNull(committeeIndex, "Committee index should not be null in attestation pool post-electra"); + assert.true(isElectraAttestation(attestation), "Attestation should be type electra.Attestation"); + } else { + assert.true(!isElectraAttestation(attestation), "Attestation should be type phase0.Attestation"); + committeeIndex = null; // For pre-electra, committee index info is encoded in attDataRootIndex + } // Pre-aggregate the contribution with existing items let aggregateByIndex = aggregateByRoot.get(attDataRootHex); @@ -144,14 +155,23 @@ export class AttestationPool { /** * For validator API to get an aggregate */ - // TODO Electra: Change attestation pool to accomodate pre-electra request getAggregate(slot: Slot, committeeIndex: CommitteeIndex, dataRootHex: RootHex): Attestation | null { + const fork = this.config.getForkName(slot); + const isPostElectra = isForkPostElectra(fork); + committeeIndex = isPostElectra ? committeeIndex : null; + const aggregate = this.aggregateByIndexByRootBySlot.get(slot)?.get(dataRootHex)?.get(committeeIndex); if (!aggregate) { // TODO: Add metric for missing aggregates return null; } + if (isPostElectra) { + assert.true(isElectraAggregate(aggregate), "Aggregate should be type AggregateFastElectra"); + } else { + assert.true(!isElectraAggregate(aggregate), "Aggregate should be type AggregateFastPhase0"); + } + return fastToAttestation(aggregate); } diff --git a/packages/beacon-node/src/chain/opPools/utils.ts b/packages/beacon-node/src/chain/opPools/utils.ts index 039e95af6c9..e136bf1d409 100644 --- a/packages/beacon-node/src/chain/opPools/utils.ts +++ b/packages/beacon-node/src/chain/opPools/utils.ts @@ -2,6 +2,7 @@ import {Signature} from "@chainsafe/blst"; import {BLS_WITHDRAWAL_PREFIX} from "@lodestar/params"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {Slot, capella} from "@lodestar/types"; +import {AggregateFast, AggregateFastElectra} from "./attestationPool.js"; /** * Prune a Map indexed by slot to keep the most recent slots, up to `slotsRetained` @@ -58,3 +59,7 @@ export function isValidBlsToExecutionChangeForBlockInclusion( return true; } + +export function isElectraAggregate(aggregate: AggregateFast): aggregate is AggregateFastElectra { + return (aggregate as AggregateFastElectra).committeeBits !== undefined; +} diff --git a/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts b/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts index 17343e386fd..a0aa6db3589 100644 --- a/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts +++ b/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts @@ -2,9 +2,14 @@ import {BitArray} from "@chainsafe/ssz"; import {CommitteeIndex, phase0, RootHex, Slot} from "@lodestar/types"; import {MapDef} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; -import {SeenAttDataKey} from "../../util/sszBytes.js"; import {InsertOutcome} from "../opPools/types.js"; +export type SeenAttDataKey = AttDataBase64 | AttDataCommitteeBitsBase64; +// pre-electra, AttestationData is used to cache attestations +type AttDataBase64 = string; +// electra, AttestationData + CommitteeBits are used to cache attestations +type AttDataCommitteeBitsBase64 = string; + export type AttestationDataCacheEntry = { // part of shuffling data, so this does not take memory committeeValidatorIndices: Uint32Array; diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 00dea299ac5..41a19eefb3f 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -9,11 +9,11 @@ import { import {IBeaconChain} from ".."; import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js"; import {RegenCaller} from "../regen/index.js"; -import {getSeenAttDataKeyFromSignedAggregateAndProof} from "../../util/sszBytes.js"; import {getSelectionProofSignatureSet, getAggregateAndProofSignatureSet} from "./signatureSets/index.js"; import { getAttestationDataSigningRoot, getCommitteeIndices, + getSeenAttDataKeyFromSignedAggregateAndProof, getShufflingForAttestationVerification, verifyHeadBlockAndTargetRoot, verifyPropagationSlotRange, @@ -71,9 +71,7 @@ async function validateAggregateAndProof( const attData = aggregate.data; const attSlot = attData.slot; - const seenAttDataKey = serializedData - ? getSeenAttDataKeyFromSignedAggregateAndProof(ForkSeq[fork], serializedData) - : null; + const seenAttDataKey = serializedData ? getSeenAttDataKeyFromSignedAggregateAndProof(fork, serializedData) : null; const cachedAttData = seenAttDataKey ? chain.seenAttestationDatas.get(attSlot, seenAttDataKey) : null; let attIndex; diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index c27b4358659..5f27fbd75d0 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -13,7 +13,14 @@ import { IndexedAttestation, } from "@lodestar/types"; import {ProtoBlock} from "@lodestar/fork-choice"; -import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, ForkName, ForkSeq, DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; +import { + ATTESTATION_SUBNET_COUNT, + SLOTS_PER_EPOCH, + ForkName, + ForkSeq, + DOMAIN_BEACON_ATTESTER, + isForkPostElectra, +} from "@lodestar/params"; import { computeEpochAtSlot, createSingleSignatureSetFromComponents, @@ -29,12 +36,15 @@ import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/in import {MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC} from "../../constants/index.js"; import {RegenCaller} from "../regen/index.js"; import { - SeenAttDataKey, getAggregationBitsFromAttestationSerialized, - getSeenAttDataKey, + getAttDataFromAttestationSerialized, + getAttDataFromSignedAggregateAndProofElectra, + getCommitteeBitsFromAttestationSerialized, + getCommitteeBitsFromSignedAggregateAndProofElectra, + getAttDataFromSignedAggregateAndProofPhase0, getSignatureFromAttestationSerialized, } from "../../util/sszBytes.js"; -import {AttestationDataCacheEntry} from "../seenCache/seenAttestationData.js"; +import {AttestationDataCacheEntry, SeenAttDataKey} from "../seenCache/seenAttestationData.js"; import {sszDeserializeAttestation} from "../../network/gossip/topic.js"; import {Result, wrapError} from "../../util/wrapError.js"; import {IBeaconChain} from "../interface.js"; @@ -66,7 +76,7 @@ export type GossipAttestation = { attSlot: Slot; // for old LIFO linear gossip queue we don't have attDataBase64 // for indexed gossip queue we have attDataBase64 - seenAttestationKey?: SeenAttDataKey | null; + attDataBase64?: SeenAttDataKey | null; }; export type Step0Result = AttestationValidationResult & { @@ -269,11 +279,7 @@ async function validateGossipAttestationNoSignatureCheck( if (attestationOrBytes.serializedData) { // gossip const attSlot = attestationOrBytes.attSlot; - attDataKey = - // we always have seenAttestationKey from the IndexedGossipQueue, getSeenAttDataKey() just for backward - // compatible in case beaconAttestationBatchValidation is false - // TODO: remove beaconAttestationBatchValidation flag since the batch attestation is stable - attestationOrBytes.seenAttestationKey ?? getSeenAttDataKey(ForkSeq[fork], attestationOrBytes.serializedData); + attDataKey = getSeenAttDataKeyFromGossipAttestation(fork, attestationOrBytes); const cachedAttData = attDataKey !== null ? chain.seenAttestationDatas.get(attSlot, attDataKey) : null; if (cachedAttData === null) { const attestation = sszDeserializeAttestation(fork, attestationOrBytes.serializedData); @@ -789,3 +795,45 @@ export function computeSubnetForSlot(shuffling: EpochShuffling, slot: number, co const committeesSinceEpochStart = shuffling.committeesPerSlot * slotsSinceEpochStart; return (committeesSinceEpochStart + committeeIndex) % ATTESTATION_SUBNET_COUNT; } + +/** + * Return fork-dependent seen attestation key + * - for pre-electra, it's the AttestationData base64 + * - for electra and later, it's the AttestationData base64 + committeeBits base64 + * + * we always have attDataBase64 from the IndexedGossipQueue, getAttDataFromAttestationSerialized() just for backward compatible when beaconAttestationBatchValidation is false + * TODO: remove beaconAttestationBatchValidation flag since the batch attestation is stable + */ +export function getSeenAttDataKeyFromGossipAttestation( + fork: ForkName, + attestation: GossipAttestation +): SeenAttDataKey | null { + const {attDataBase64, serializedData} = attestation; + if (isForkPostElectra(fork)) { + const attData = attDataBase64 ?? getAttDataFromAttestationSerialized(serializedData); + const committeeBits = getCommitteeBitsFromAttestationSerialized(serializedData); + return attData && committeeBits ? attDataBase64 + committeeBits : null; + } + + // pre-electra + return attDataBase64 ?? getAttDataFromAttestationSerialized(serializedData); +} + +/** + * Extract attestation data key from SignedAggregateAndProof Uint8Array to use cached data from SeenAttestationDatas + * - for pre-electra, it's the AttestationData base64 + * - for electra and later, it's the AttestationData base64 + committeeBits base64 + */ +export function getSeenAttDataKeyFromSignedAggregateAndProof( + fork: ForkName, + aggregateAndProof: Uint8Array +): SeenAttDataKey | null { + if (isForkPostElectra(fork)) { + const attData = getAttDataFromSignedAggregateAndProofElectra(aggregateAndProof); + const committeeBits = getCommitteeBitsFromSignedAggregateAndProofElectra(aggregateAndProof); + return attData && committeeBits ? attData + committeeBits : null; + } + + // pre-electra + return getAttDataFromSignedAggregateAndProofPhase0(aggregateAndProof); +} diff --git a/packages/beacon-node/src/network/processor/gossipQueues/index.ts b/packages/beacon-node/src/network/processor/gossipQueues/index.ts index b38ee74279c..347458c9144 100644 --- a/packages/beacon-node/src/network/processor/gossipQueues/index.ts +++ b/packages/beacon-node/src/network/processor/gossipQueues/index.ts @@ -1,8 +1,7 @@ import {mapValues} from "@lodestar/utils"; -import {ForkSeq} from "@lodestar/params"; import {GossipType} from "../../gossip/interface.js"; import {PendingGossipsubMessage} from "../types.js"; -import {getSeenAttDataKey} from "../../../util/sszBytes.js"; +import {getGossipAttestationIndex} from "../../../util/sszBytes.js"; import {LinearGossipQueue} from "./linear.js"; import { DropType, @@ -88,8 +87,8 @@ const indexedGossipQueueOpts: { [GossipType.beacon_attestation]: { maxLength: 24576, indexFn: (item: PendingGossipsubMessage) => { - const {topic, msg} = item; - return getSeenAttDataKey(ForkSeq[topic.fork], msg.data); + // Note indexFn is fork agnostic despite changes introduced in Electra + return getGossipAttestationIndex(item.msg.data); }, minChunkSize: MIN_SIGNATURE_SETS_TO_BATCH_VERIFY, maxChunkSize: MAX_GOSSIP_ATTESTATION_BATCH_SIZE, diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index 52b73260c89..3c47eb85c31 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -10,11 +10,10 @@ import { } from "@lodestar/params"; export type BlockRootHex = RootHex; -export type SeenAttDataKey = AttDataBase64 | AttDataCommitteeBitsBase64; // pre-electra, AttestationData is used to cache attestations export type AttDataBase64 = string; -// electra, AttestationData + CommitteeBits are used to cache attestations -export type AttDataCommitteeBitsBase64 = string; +// electra, CommitteeBits +export type CommitteeBitsBase64 = string; // pre-electra // class Attestation(Container): @@ -71,43 +70,22 @@ export function getBlockRootFromAttestationSerialized(data: Uint8Array): BlockRo } /** - * Extract attestation data key from an attestation Uint8Array in order to index gossip queue and cache later in SeenAttestationDatas - */ -export function getSeenAttDataKey(forkSeq: ForkSeq, data: Uint8Array): SeenAttDataKey | null { - return forkSeq >= ForkSeq.electra ? getSeenAttDataKeyElectra(data) : getSeenAttDataKeyPhase0(data); -} - -/** - * Extract attestation data + committeeBits base64 from electra attestation serialized bytes. + * Extract attestation data base64 from all forks' attestation serialized bytes. * Return null if data is not long enough to extract attestation data. */ -export function getSeenAttDataKeyElectra(electraAttestationBytes: Uint8Array): AttDataCommitteeBitsBase64 | null { - const attestationData = getSeenAttDataKeyPhase0(electraAttestationBytes); - - if (attestationData === null) { - return null; - } - - const committeeBits = getCommitteeBitsFromAttestationSerialized(electraAttestationBytes); - - if (committeeBits === null) { +export function getAttDataFromAttestationSerialized(data: Uint8Array): AttDataBase64 | null { + if (data.length < VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE) { return null; } - return attestationData + toBase64(committeeBits.uint8Array); + return toBase64(data.subarray(VARIABLE_FIELD_OFFSET, VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE)); } /** - * Extract attestation data base64 from phase0 attestation serialized bytes. - * Return null if data is not long enough to extract attestation data. + * Alias of `getAttDataFromAttestationSerialized` specifically for batch handling indexing in gossip queue */ -export function getSeenAttDataKeyPhase0(data: Uint8Array): AttDataBase64 | null { - if (data.length < VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE) { - return null; - } - - // base64 is a bit efficient than hex - return toBase64(data.slice(VARIABLE_FIELD_OFFSET, VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE)); +export function getGossipAttestationIndex(data: Uint8Array): AttDataBase64 | null { + return getAttDataFromAttestationSerialized(data); } /** @@ -146,7 +124,7 @@ export function getSignatureFromAttestationSerialized(data: Uint8Array): BLSSign * Extract committee bits from Electra attestation serialized bytes. * Return null if data is not long enough to extract committee bits. */ -export function getCommitteeBitsFromAttestationSerialized(data: Uint8Array): BitArray | null { +export function getCommitteeBitsFromAttestationSerialized(data: Uint8Array): CommitteeBitsBase64 | null { const committeeBitsStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; if (data.length < committeeBitsStartIndex + COMMITTEE_BITS_SIZE) { @@ -155,7 +133,7 @@ export function getCommitteeBitsFromAttestationSerialized(data: Uint8Array): Bit const uint8Array = data.subarray(committeeBitsStartIndex, committeeBitsStartIndex + COMMITTEE_BITS_SIZE); - return new BitArray(uint8Array, MAX_COMMITTEES_PER_SLOT); + return toBase64(uint8Array); } // @@ -205,49 +183,49 @@ export function getBlockRootFromSignedAggregateAndProofSerialized(data: Uint8Arr } /** - * Extract attestation data key from SignedAggregateAndProof Uint8Array to use cached data from SeenAttestationDatas - */ -export function getSeenAttDataKeyFromSignedAggregateAndProof( - forkSeq: ForkSeq, - data: Uint8Array -): SeenAttDataKey | null { - return forkSeq >= ForkSeq.electra - ? getSeenAttDataKeyFromSignedAggregateAndProofElectra(data) - : getSeenAttDataKeyFromSignedAggregateAndProofPhase0(data); -} - -/** - * Extract AttestationData + CommitteeBits from SignedAggregateAndProof for electra + * Extract AttestationData base64 from SignedAggregateAndProof for electra * Return null if data is not long enough */ -export function getSeenAttDataKeyFromSignedAggregateAndProofElectra(data: Uint8Array): SeenAttDataKey | null { +export function getAttDataFromSignedAggregateAndProofElectra(data: Uint8Array): AttDataBase64 | null { const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET; const endIndex = startIndex + ATTESTATION_DATA_SIZE; if (data.length < endIndex + SIGNATURE_SIZE + COMMITTEE_BITS_SIZE) { return null; } + return toBase64(data.subarray(startIndex, endIndex)); +} - // base64 is a bit efficient than hex +/** + * Extract CommitteeBits base64 from SignedAggregateAndProof for electra + * Return null if data is not long enough + */ +export function getCommitteeBitsFromSignedAggregateAndProofElectra(data: Uint8Array): CommitteeBitsBase64 | null { + const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; + const endIndex = startIndex + COMMITTEE_BITS_SIZE; + + if (data.length < endIndex) { + return null; + } - return Buffer.concat([ - data.subarray(startIndex, endIndex), - data.subarray(endIndex + SIGNATURE_SIZE, endIndex + SIGNATURE_SIZE + COMMITTEE_BITS_SIZE), - ]).toString("base64"); + return toBase64(data.subarray(startIndex, endIndex)); } /** * Extract attestation data base64 from signed aggregate and proof serialized bytes. * Return null if data is not long enough to extract attestation data. */ -export function getSeenAttDataKeyFromSignedAggregateAndProofPhase0(data: Uint8Array): AttDataBase64 | null { +export function getAttDataFromSignedAggregateAndProofPhase0(data: Uint8Array): AttDataBase64 | null { if (data.length < SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE) { return null; } // base64 is a bit efficient than hex return toBase64( - data.slice(SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET, SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE) + data.subarray( + SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET, + SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE + ) ); } @@ -316,6 +294,9 @@ function checkSlotHighBytes(data: Uint8Array, offset: number): boolean { return (data[offset + 4] | data[offset + 5] | data[offset + 6] | data[offset + 7]) === 0; } +/** + * base64 is a bit more efficient than hex + */ function toBase64(data: Uint8Array): string { return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("base64"); } diff --git a/packages/beacon-node/test/mocks/clock.ts b/packages/beacon-node/test/mocks/clock.ts index c38794bf16d..6f09bd29249 100644 --- a/packages/beacon-node/test/mocks/clock.ts +++ b/packages/beacon-node/test/mocks/clock.ts @@ -74,5 +74,6 @@ export function getMockedClock(): Mocked { }, currentSlotWithGossipDisparity: undefined, isCurrentSlotGivenGossipDisparity: vi.fn(), + secFromSlot: vi.fn(), } as unknown as Mocked; } diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index cc85cfd7d55..addeacf26a8 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -124,7 +124,7 @@ vi.mock("../../src/chain/chain.js", async (importActual) => { // @ts-expect-error eth1: new Eth1ForBlockProduction(), opPool: new OpPool(), - aggregatedAttestationPool: new AggregatedAttestationPool(), + aggregatedAttestationPool: new AggregatedAttestationPool(config), // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error beaconProposerCache: new BeaconProposerCache(), diff --git a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts index ee14eb27b51..e4e69fdf363 100644 --- a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts @@ -10,8 +10,9 @@ import { import {HISTORICAL_ROOTS_LIMIT, SLOTS_PER_EPOCH} from "@lodestar/params"; import {ExecutionStatus, ForkChoice, IForkChoiceStore, ProtoArray, DataAvailabilityStatus} from "@lodestar/fork-choice"; import {ssz} from "@lodestar/types"; -// eslint-disable-next-line import/no-relative-packages -import {generatePerfTestCachedStateAltair} from "../../../../../state-transition/test/perf/util.js"; + +import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {generatePerfTestCachedStateAltair} from "@lodestar/state-transition/test/perf/util.js"; import {AggregatedAttestationPool} from "../../../../src/chain/opPools/aggregatedAttestationPool.js"; import {computeAnchorCheckpoint} from "../../../../src/chain/initState.js"; @@ -230,7 +231,9 @@ function getAggregatedAttestationPool( numMissedVotes: number, numBadVotes: number ): AggregatedAttestationPool { - const pool = new AggregatedAttestationPool(); + const config = createChainForkConfig(defaultChainConfig); + + const pool = new AggregatedAttestationPool(config); for (let epochSlot = 0; epochSlot < SLOTS_PER_EPOCH; epochSlot++) { const slot = state.slot - 1 - epochSlot; const epoch = computeEpochAtSlot(slot); diff --git a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts index e0e0a1e5116..f285317474d 100644 --- a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts +++ b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts @@ -5,7 +5,7 @@ import {ssz} from "@lodestar/types"; import {generateTestCachedBeaconStateOnlyValidators} from "../../../../../state-transition/test/perf/util.js"; import {validateAttestation, validateGossipAttestationsSameAttData} from "../../../../src/chain/validation/index.js"; import {getAttestationValidData} from "../../../utils/validationData/attestation.js"; -import {getSeenAttDataKeyPhase0} from "../../../../src/util/sszBytes.js"; +import {getAttDataFromAttestationSerialized} from "../../../../src/util/sszBytes.js"; describe("validate gossip attestation", () => { setBenchOpts({ @@ -42,7 +42,7 @@ describe("validate gossip attestation", () => { attestation: null, serializedData, attSlot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet0 ); @@ -67,7 +67,7 @@ describe("validate gossip attestation", () => { attestation: null, serializedData, attSlot, - attDataBase64: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }; }); diff --git a/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts index c9c128e5485..f00a300bbe4 100644 --- a/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts @@ -11,6 +11,7 @@ import { } from "@lodestar/params"; import {ssz, phase0} from "@lodestar/types"; import {CachedBeaconStateAltair} from "@lodestar/state-transition/src/types.js"; +import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; import {MockedForkChoice, getMockedForkChoice} from "../../../mocks/mockedBeaconChain.js"; import { aggregateConsolidation, @@ -36,6 +37,9 @@ const validSignature = fromHexString( describe("AggregatedAttestationPool", function () { let pool: AggregatedAttestationPool; const fork = ForkName.altair; + const config = createChainForkConfig({ + ...defaultChainConfig, + }); const altairForkEpoch = 2020; const currentEpoch = altairForkEpoch + 10; const currentSlot = SLOTS_PER_EPOCH * currentEpoch; @@ -79,7 +83,7 @@ describe("AggregatedAttestationPool", function () { let forkchoiceStub: MockedForkChoice; beforeEach(() => { - pool = new AggregatedAttestationPool(); + pool = new AggregatedAttestationPool(config); altairState = originalState.clone(); forkchoiceStub = getMockedForkChoice(); }); diff --git a/packages/beacon-node/test/unit/chain/opPools/attestationPool.test.ts b/packages/beacon-node/test/unit/chain/opPools/attestationPool.test.ts new file mode 100644 index 00000000000..68efd075158 --- /dev/null +++ b/packages/beacon-node/test/unit/chain/opPools/attestationPool.test.ts @@ -0,0 +1,120 @@ +import {fromHexString, toHexString} from "@chainsafe/ssz"; +import {describe, it, expect, beforeEach, vi} from "vitest"; +import {GENESIS_SLOT, SLOTS_PER_EPOCH} from "@lodestar/params"; +import {ssz} from "@lodestar/types"; +import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {InsertOutcome} from "../../../../src/chain/opPools/types.js"; +import {AttestationPool} from "../../../../src/chain/opPools/attestationPool.js"; +import {getMockedClock} from "../../../mocks/clock.js"; + +/** Valid signature of random data to prevent BLS errors */ +export const validSignature = fromHexString( + "0xb2afb700f6c561ce5e1b4fedaec9d7c06b822d38c720cf588adfda748860a940adf51634b6788f298c552de40183b5a203b2bbe8b7dd147f0bb5bc97080a12efbb631c8888cb31a99cc4706eb3711865b8ea818c10126e4d818b542e9dbf9ae8" +); + +describe("AttestationPool", function () { + /* eslint-disable @typescript-eslint/naming-convention */ + const config = createChainForkConfig({ + ...defaultChainConfig, + ELECTRA_FORK_EPOCH: 5, + DENEB_FORK_EPOCH: 4, + CAPELLA_FORK_EPOCH: 3, + BELLATRIX_FORK_EPOCH: 2, + ALTAIR_FORK_EPOCH: 1, + }); + const clockStub = getMockedClock(); + vi.spyOn(clockStub, "secFromSlot").mockReturnValue(0); + + const cutOffSecFromSlot = (2 / 3) * config.SECONDS_PER_SLOT; + + // Mock attestations + const electraAttestationData = { + ...ssz.phase0.AttestationData.defaultValue(), + slot: config.ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH, + }; + const electraAttestation = { + ...ssz.electra.Attestation.defaultValue(), + data: electraAttestationData, + signature: validSignature, + }; + const phase0AttestationData = {...ssz.phase0.AttestationData.defaultValue(), slot: GENESIS_SLOT}; + const phase0Attestation = { + ...ssz.phase0.Attestation.defaultValue(), + data: phase0AttestationData, + signature: validSignature, + }; + + let pool: AttestationPool; + + beforeEach(() => { + pool = new AttestationPool(config, clockStub, cutOffSecFromSlot); + }); + + it("add correct electra attestation", () => { + const committeeIndex = 0; + const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraAttestation.data)); + const outcome = pool.add(committeeIndex, electraAttestation, attDataRootHex); + + expect(outcome).equal(InsertOutcome.NewData); + expect(pool.getAggregate(electraAttestationData.slot, committeeIndex, attDataRootHex)).toEqual(electraAttestation); + }); + + it("add correct phase0 attestation", () => { + const committeeIndex = null; + const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0Attestation.data)); + const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex); + + expect(outcome).equal(InsertOutcome.NewData); + expect(pool.getAggregate(phase0AttestationData.slot, committeeIndex, attDataRootHex)).toEqual(phase0Attestation); + expect(pool.getAggregate(phase0AttestationData.slot, 10, attDataRootHex)).toEqual(phase0Attestation); + expect(pool.getAggregate(phase0AttestationData.slot, 42, attDataRootHex)).toEqual(phase0Attestation); + expect(pool.getAggregate(phase0AttestationData.slot, null, attDataRootHex)).toEqual(phase0Attestation); + }); + + it("add electra attestation without committee index", () => { + const committeeIndex = null; + const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraAttestation.data)); + + expect(() => pool.add(committeeIndex, electraAttestation, attDataRootHex)).toThrow(); + expect(pool.getAggregate(electraAttestationData.slot, committeeIndex, attDataRootHex)).toBeNull(); + }); + + it("add phase0 attestation with committee index", () => { + const committeeIndex = 0; + const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0Attestation.data)); + const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex); + + expect(outcome).equal(InsertOutcome.NewData); + expect(pool.getAggregate(phase0AttestationData.slot, committeeIndex, attDataRootHex)).toEqual(phase0Attestation); + expect(pool.getAggregate(phase0AttestationData.slot, 123, attDataRootHex)).toEqual(phase0Attestation); + expect(pool.getAggregate(phase0AttestationData.slot, 456, attDataRootHex)).toEqual(phase0Attestation); + expect(pool.getAggregate(phase0AttestationData.slot, null, attDataRootHex)).toEqual(phase0Attestation); + }); + + it("add electra attestation with phase0 slot", () => { + const electraAttestationDataWithPhase0Slot = {...ssz.phase0.AttestationData.defaultValue(), slot: GENESIS_SLOT}; + const attestation = { + ...ssz.electra.Attestation.defaultValue(), + data: electraAttestationDataWithPhase0Slot, + signature: validSignature, + }; + const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraAttestationDataWithPhase0Slot)); + + expect(() => pool.add(0, attestation, attDataRootHex)).toThrow(); + }); + + it("add phase0 attestation with electra slot", () => { + const phase0AttestationDataWithElectraSlot = { + ...ssz.phase0.AttestationData.defaultValue(), + slot: config.ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH, + }; + const attestation = { + ...ssz.phase0.Attestation.defaultValue(), + data: phase0AttestationDataWithElectraSlot, + signature: validSignature, + }; + const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0AttestationDataWithElectraSlot)); + + expect(() => pool.add(0, attestation, attDataRootHex)).toThrow(); + }); +}); diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts index 4a1c3badae5..45d293ffbb3 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts @@ -1,6 +1,6 @@ import {BitArray} from "@chainsafe/ssz"; -import {describe, it} from "vitest"; -import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {describe, expect, it} from "vitest"; +import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; import {ssz} from "@lodestar/types"; // eslint-disable-next-line import/no-relative-packages import {generateTestCachedBeaconStateOnlyValidators} from "../../../../../../state-transition/test/perf/util.js"; @@ -9,14 +9,17 @@ import {IBeaconChain} from "../../../../../src/chain/index.js"; import { ApiAttestation, GossipAttestation, + getSeenAttDataKeyFromGossipAttestation, + getSeenAttDataKeyFromSignedAggregateAndProof, validateApiAttestation, validateAttestation, } from "../../../../../src/chain/validation/index.js"; -import {getSeenAttDataKeyPhase0} from "../../../../../src/util/sszBytes.js"; +import {getAttDataFromAttestationSerialized} from "../../../../../src/util/sszBytes.js"; import {memoOnce} from "../../../../utils/cache.js"; import {expectRejectedWithLodestarError} from "../../../../utils/errors.js"; import {AttestationValidDataOpts, getAttestationValidData} from "../../../../utils/validationData/attestation.js"; +// TODO: more tests for electra describe("validateAttestation", () => { const vc = 64; const stateSlot = 100; @@ -52,7 +55,7 @@ describe("validateAttestation", () => { const {chain, subnet} = getValidData(); await expectGossipError( chain, - {attestation: null, serializedData: Buffer.alloc(0), attSlot: 0, seenAttestationKey: "invalid"}, + {attestation: null, serializedData: Buffer.alloc(0), attSlot: 0, attDataBase64: "invalid"}, subnet, GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE ); @@ -72,7 +75,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.BAD_TARGET_EPOCH @@ -91,7 +94,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.PAST_SLOT @@ -110,7 +113,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.FUTURE_SLOT @@ -135,7 +138,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET @@ -155,7 +158,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET @@ -179,7 +182,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT @@ -199,7 +202,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.INVALID_TARGET_ROOT @@ -226,7 +229,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS @@ -245,7 +248,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, invalidSubnet, AttestationErrorCode.INVALID_SUBNET_ID @@ -265,7 +268,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.ATTESTATION_ALREADY_KNOWN @@ -287,7 +290,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), + attDataBase64: getAttDataFromAttestationSerialized(serializedData), }, subnet, AttestationErrorCode.INVALID_SIGNATURE @@ -314,3 +317,53 @@ describe("validateAttestation", () => { await expectRejectedWithLodestarError(validateAttestation(fork, chain, attestationOrBytes, subnet), errorCode); } }); + +describe("getSeenAttDataKey", () => { + const slot = 100; + const index = 0; + const blockRoot = Buffer.alloc(32, 1); + + it("phase0", () => { + const attestationData = ssz.phase0.AttestationData.defaultValue(); + attestationData.slot = slot; + attestationData.index = index; + attestationData.beaconBlockRoot = blockRoot; + const attestation = ssz.phase0.Attestation.defaultValue(); + attestation.data = attestationData; + const attDataBase64 = Buffer.from(ssz.phase0.AttestationData.serialize(attestationData)).toString("base64"); + const attestationBytes = ssz.phase0.Attestation.serialize(attestation); + const gossipAttestation = {attDataBase64, serializedData: attestationBytes, attSlot: slot} as GossipAttestation; + + const signedAggregateAndProof = ssz.phase0.SignedAggregateAndProof.defaultValue(); + signedAggregateAndProof.message.aggregate.data.slot = slot; + signedAggregateAndProof.message.aggregate.data.index = index; + signedAggregateAndProof.message.aggregate.data.beaconBlockRoot = blockRoot; + const aggregateAndProofBytes = ssz.phase0.SignedAggregateAndProof.serialize(signedAggregateAndProof); + + expect(getSeenAttDataKeyFromGossipAttestation(ForkName.phase0, gossipAttestation)).toEqual( + getSeenAttDataKeyFromSignedAggregateAndProof(ForkName.phase0, aggregateAndProofBytes) + ); + }); + + it("electra", () => { + const attestationData = ssz.phase0.AttestationData.defaultValue(); + attestationData.slot = slot; + attestationData.index = index; + attestationData.beaconBlockRoot = blockRoot; + const attestation = ssz.electra.Attestation.defaultValue(); + attestation.data = attestationData; + const attDataBase64 = Buffer.from(ssz.phase0.AttestationData.serialize(attestationData)).toString("base64"); + const attestationBytes = ssz.electra.Attestation.serialize(attestation); + const gossipAttestation = {attDataBase64, serializedData: attestationBytes, attSlot: slot} as GossipAttestation; + + const signedAggregateAndProof = ssz.electra.SignedAggregateAndProof.defaultValue(); + signedAggregateAndProof.message.aggregate.data.slot = slot; + signedAggregateAndProof.message.aggregate.data.index = index; + signedAggregateAndProof.message.aggregate.data.beaconBlockRoot = blockRoot; + const aggregateAndProofBytes = ssz.electra.SignedAggregateAndProof.serialize(signedAggregateAndProof); + + expect(getSeenAttDataKeyFromGossipAttestation(ForkName.electra, gossipAttestation)).toEqual( + getSeenAttDataKeyFromSignedAggregateAndProof(ForkName.electra, aggregateAndProofBytes) + ); + }); +}); diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index 643b6b1bac0..88e1d58c034 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -4,8 +4,8 @@ import {deneb, electra, Epoch, isElectraAttestation, phase0, RootHex, Slot, ssz} import {fromHex, toHex} from "@lodestar/utils"; import {ForkName, MAX_COMMITTEES_PER_SLOT} from "@lodestar/params"; import { - getSeenAttDataKeyPhase0, - getSeenAttDataKeyFromSignedAggregateAndProofPhase0, + getAttDataFromAttestationSerialized, + getAttDataFromSignedAggregateAndProofPhase0, getAggregationBitsFromAttestationSerialized as getAggregationBitsFromAttestationSerialized, getBlockRootFromAttestationSerialized, getBlockRootFromSignedAggregateAndProofSerialized, @@ -15,7 +15,8 @@ import { getSlotFromSignedBeaconBlockSerialized, getSlotFromBlobSidecarSerialized, getCommitteeBitsFromAttestationSerialized, - getSeenAttDataKeyFromSignedAggregateAndProofElectra, + getCommitteeBitsFromSignedAggregateAndProofElectra, + getAttDataFromSignedAggregateAndProofElectra, } from "../../../src/util/sszBytes.js"; describe("attestation SSZ serialized picking", () => { @@ -53,7 +54,9 @@ describe("attestation SSZ serialized picking", () => { expect(getAggregationBitsFromAttestationSerialized(ForkName.electra, bytes)?.toBoolArray()).toEqual( attestation.aggregationBits.toBoolArray() ); - expect(getCommitteeBitsFromAttestationSerialized(bytes)).toEqual(attestation.committeeBits); + expect(getCommitteeBitsFromAttestationSerialized(bytes)).toEqual( + Buffer.from(attestation.committeeBits.uint8Array).toString("base64") + ); expect(getSignatureFromAttestationSerialized(bytes)).toEqual(attestation.signature); } else { expect(getAggregationBitsFromAttestationSerialized(ForkName.phase0, bytes)?.toBoolArray()).toEqual( @@ -63,7 +66,7 @@ describe("attestation SSZ serialized picking", () => { } const attDataBase64 = ssz.phase0.AttestationData.serialize(attestation.data); - expect(getSeenAttDataKeyPhase0(bytes)).toBe(Buffer.from(attDataBase64).toString("base64")); + expect(getAttDataFromAttestationSerialized(bytes)).toBe(Buffer.from(attDataBase64).toString("base64")); }); } @@ -81,10 +84,10 @@ describe("attestation SSZ serialized picking", () => { } }); - it("getAttDataBase64FromAttestationSerialized - invalid data", () => { + it("getAttDataFromAttestationSerialized - invalid data", () => { const invalidAttDataBase64DataSizes = [0, 4, 100, 128, 131]; for (const size of invalidAttDataBase64DataSizes) { - expect(getSeenAttDataKeyPhase0(Buffer.alloc(size))).toBeNull(); + expect(getAttDataFromAttestationSerialized(Buffer.alloc(size))).toBeNull(); } }); @@ -128,9 +131,7 @@ describe("phase0 SignedAggregateAndProof SSZ serialized picking", () => { ); const attDataBase64 = ssz.phase0.AttestationData.serialize(signedAggregateAndProof.message.aggregate.data); - expect(getSeenAttDataKeyFromSignedAggregateAndProofPhase0(bytes)).toBe( - Buffer.from(attDataBase64).toString("base64") - ); + expect(getAttDataFromSignedAggregateAndProofPhase0(bytes)).toBe(Buffer.from(attDataBase64).toString("base64")); }); } @@ -151,7 +152,7 @@ describe("phase0 SignedAggregateAndProof SSZ serialized picking", () => { it("getAttDataBase64FromSignedAggregateAndProofSerialized - invalid data", () => { const invalidAttDataBase64DataSizes = [0, 4, 100, 128, 339]; for (const size of invalidAttDataBase64DataSizes) { - expect(getSeenAttDataKeyFromSignedAggregateAndProofPhase0(Buffer.alloc(size))).toBeNull(); + expect(getAttDataFromSignedAggregateAndProofPhase0(Buffer.alloc(size))).toBeNull(); } }); }); @@ -182,8 +183,11 @@ describe("electra SignedAggregateAndProof SSZ serialized picking", () => { const committeeBits = ssz.electra.CommitteeBits.serialize( signedAggregateAndProof.message.aggregate.committeeBits ); - const seenKey = Buffer.concat([attDataBase64, committeeBits]).toString("base64"); - expect(getSeenAttDataKeyFromSignedAggregateAndProofElectra(bytes)).toBe(seenKey); + + expect(getAttDataFromSignedAggregateAndProofElectra(bytes)).toBe(Buffer.from(attDataBase64).toString("base64")); + expect(getCommitteeBitsFromSignedAggregateAndProofElectra(bytes)).toBe( + Buffer.from(committeeBits).toString("base64") + ); }); } @@ -204,7 +208,7 @@ describe("electra SignedAggregateAndProof SSZ serialized picking", () => { it("getAttDataBase64FromSignedAggregateAndProofSerialized - invalid data", () => { const invalidAttDataBase64DataSizes = [0, 4, 100, 128, 339]; for (const size of invalidAttDataBase64DataSizes) { - expect(getSeenAttDataKeyFromSignedAggregateAndProofPhase0(Buffer.alloc(size))).toBeNull(); + expect(getAttDataFromSignedAggregateAndProofPhase0(Buffer.alloc(size))).toBeNull(); } }); it("getSlotFromSignedAggregateAndProofSerialized - invalid data - large slots", () => { diff --git a/packages/state-transition/src/block/processAttestationPhase0.ts b/packages/state-transition/src/block/processAttestationPhase0.ts index 616c1a943af..9ed25425678 100644 --- a/packages/state-transition/src/block/processAttestationPhase0.ts +++ b/packages/state-transition/src/block/processAttestationPhase0.ts @@ -88,7 +88,6 @@ export function validateAttestation(fork: ForkSeq, state: CachedBeaconStateAllFo if (fork >= ForkSeq.electra) { assert.equal(data.index, 0, `AttestationData.index must be zero: index=${data.index}`); const attestationElectra = attestation as electra.Attestation; - // TODO Electra: this should be obsolete soon when the spec switches to committeeIndices const committeeIndices = attestationElectra.committeeBits.getTrueBitIndexes(); if (committeeIndices.length === 0) {