diff --git a/packages/api/src/beacon/routes/beacon/pool.ts b/packages/api/src/beacon/routes/beacon/pool.ts index 65467e98b721..b98575e4d4cc 100644 --- a/packages/api/src/beacon/routes/beacon/pool.ts +++ b/packages/api/src/beacon/routes/beacon/pool.ts @@ -1,4 +1,4 @@ -import {phase0, altair, capella, CommitteeIndex, Slot, ssz} from "@lodestar/types"; +import {phase0, altair, capella, CommitteeIndex, Slot, ssz, allForks} from "@lodestar/types"; import {ApiClientResponse} from "../../../interfaces.js"; import {HttpStatusCode} from "../../../utils/client/httpStatusCode.js"; import { @@ -80,7 +80,7 @@ export type Api = { * @throws ApiError */ submitPoolAttestations( - attestations: phase0.Attestation[] + attestations: allForks.Attestation[] ): Promise>; /** diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 1d6b8b80551c..de5b1e0e30e1 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -341,7 +341,7 @@ export type Api = { slot: Slot ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: phase0.Attestation}}, + {[HttpStatusCode.OK]: {data: allForks.Attestation; version: ForkName}}, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -354,7 +354,7 @@ export type Api = { * @throws ApiError */ publishAggregateAndProofs( - signedAggregateAndProofs: phase0.SignedAggregateAndProof[] + signedAggregateAndProofs: allForks.SignedAggregateAndProof[] // TODO Electra: Add version ): Promise>; publishContributionAndProofs( @@ -786,7 +786,7 @@ export function getReturnTypes(): ReturnTypes { produceAttestationData: ContainerData(ssz.phase0.AttestationData), produceSyncCommitteeContribution: ContainerData(ssz.altair.SyncCommitteeContribution), - getAggregatedAttestation: ContainerData(ssz.phase0.Attestation), + getAggregatedAttestation: WithVersion((fork) => ssz.allForks[fork].Attestation), submitBeaconCommitteeSelections: ContainerData(ArrayOf(BeaconCommitteeSelection)), submitSyncCommitteeSelections: ContainerData(ArrayOf(SyncCommitteeSelection)), getLiveness: jsonType("snake"), 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 09a66eba4d15..50936fe407bb 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -1,6 +1,6 @@ import {routes, ServerApi} from "@lodestar/api"; import {Epoch, ssz} from "@lodestar/types"; -import {SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params"; +import {ForkName, SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params"; import {validateApiAttestation} from "../../../../chain/validation/index.js"; import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js"; import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js"; @@ -77,7 +77,7 @@ export function getBeaconPoolApi({ metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome}); } - chain.emitter.emit(routes.events.EventType.attestation, attestation); + chain.emitter.emit(routes.events.EventType.attestation, {data: attestation, version: ForkName.phase0}); const sentPeers = await network.publishBeaconAttestation(attestation, subnet); metrics?.onPoolSubmitUnaggregatedAttestation(seenTimestampSec, indexedAttestation, subnet, sentPeers); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index f2a3f1faca0a..0ae526835c37 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -819,6 +819,7 @@ export function getValidatorApi({ const attEpoch = computeEpochAtSlot(slot); const headBlockRootHex = chain.forkChoice.getHead().blockRoot; const headBlockRoot = fromHexString(headBlockRootHex); + const fork = config.getForkSeq(slot); const beaconBlockRoot = slot >= headSlot @@ -846,7 +847,7 @@ export function getValidatorApi({ return { data: { slot, - index: committeeIndex, + index: fork >= ForkSeq.electra ? 0 : committeeIndex, beaconBlockRoot, source: attEpochState.currentJustifiedCheckpoint, target: {epoch: attEpoch, root: targetRoot}, @@ -1078,6 +1079,7 @@ export function getValidatorApi({ return { data: aggregate, + version: config.getForkName(slot), }; }, diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 6296a3e517ae..de7faf183237 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -68,7 +68,7 @@ export async function importBlock( const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; const blockDelaySec = (fullyVerifiedBlock.seenTimestampSec - postState.genesisTime) % this.config.SECONDS_PER_SLOT; const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000); - const fork = postState.config.getForkSeq(blockSlot); + const fork = this.config.getForkSeq(blockSlot); // this is just a type assertion since blockinput with blobsPromise type will not end up here if (blockInput.type === BlockInputType.blobsPromise) { @@ -425,7 +425,10 @@ export async function importBlock( } if (this.emitter.listenerCount(routes.events.EventType.attestation)) { for (const attestation of block.message.body.attestations) { - this.emitter.emit(routes.events.EventType.attestation, attestation); + this.emitter.emit(routes.events.EventType.attestation, { + version: this.config.getForkName(blockSlot), + data: attestation, + }); } } if (this.emitter.listenerCount(routes.events.EventType.attesterSlashing)) { diff --git a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts index 6dfb806ee3e3..d56fe294f660 100644 --- a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts @@ -1,7 +1,14 @@ import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; -import {ForkName, ForkSeq, MAX_ATTESTATIONS, MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH} from "@lodestar/params"; -import {phase0, Epoch, Slot, ssz, ValidatorIndex, RootHex, allForks} from "@lodestar/types"; +import { + ForkName, + ForkSeq, + MAX_ATTESTATIONS, + MAX_ATTESTATIONS_ELECTRA, + MIN_ATTESTATION_INCLUSION_DELAY, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; +import {phase0, Epoch, Slot, ssz, ValidatorIndex, RootHex, allForks, electra} from "@lodestar/types"; import { CachedBeaconStateAllForks, CachedBeaconStatePhase0, @@ -39,7 +46,7 @@ type ValidateAttestationDataFn = (attData: phase0.AttestationData) => boolean; const MAX_RETAINED_ATTESTATIONS_PER_GROUP = 4; /** - * On mainnet, each slot has 64 committees, and each block has 128 attestations max so in average + * On mainnet, each slot has 64 committees, and each block has 128 (8 in electra) attestations max so in average * we get 2 attestation per groups. * Starting from Jan 2024, we have a performance issue getting attestations for a block. Based on the * fact that lot of groups will have only 1 attestation since it's full of participation increase this number @@ -220,16 +227,34 @@ export class AggregatedAttestationPool { } } - const sortedAttestationsByScore = attestationsByScore.sort((a, b) => b.score - a.score); - const attestationsForBlock: allForks.Attestation[] = []; - for (const [i, attestationWithScore] of sortedAttestationsByScore.entries()) { - if (i >= MAX_ATTESTATIONS) { - break; + if (ForkSeq[fork] >= ForkSeq.electra) { + // In Electra, we further pack attestations with same attestationData from different committee + const sortedAttestationsByScore = this.aggregateAttestationsByScore(attestationsByScore).sort( + (a, b) => b.score - a.score + ); + const attestationsForBlock: electra.Attestation[] = []; + for (const [i, attestationWithScore] of sortedAttestationsByScore.entries()) { + if (i >= MAX_ATTESTATIONS_ELECTRA) { + break; + } + // attestations could be modified in this op pool, so we need to clone for block + attestationsForBlock.push( + ssz.electra.Attestation.clone(attestationWithScore.attestation as electra.Attestation) + ); + } + return attestationsForBlock; + } else { + const sortedAttestationsByScore = attestationsByScore.sort((a, b) => b.score - a.score); + const attestationsForBlock: phase0.Attestation[] = []; + for (const [i, attestationWithScore] of sortedAttestationsByScore.entries()) { + if (i >= MAX_ATTESTATIONS) { + break; + } + // attestations could be modified in this op pool, so we need to clone for block + attestationsForBlock.push(ssz.phase0.Attestation.clone(attestationWithScore.attestation)); } - // attestations could be modified in this op pool, so we need to clone for block - attestationsForBlock.push(ssz.allForks[fork].Attestation.clone(attestationWithScore.attestation)); + return attestationsForBlock; } - return attestationsForBlock; } /** @@ -256,6 +281,16 @@ export class AggregatedAttestationPool { } return attestations; } + + /** + * Electra and after: Block proposer consolidates attestations with the same + * attestation data from different committee into a single attestation + * https://github.com/ethereum/consensus-specs/blob/aba6345776aa876dad368cab27fbbb23fae20455/specs/_features/eip7549/validator.md?plain=1#L39 + * TODO Electra: implement this or consider other strategy + */ + private aggregateAttestationsByScore(attestationsByScore: AttestationWithScore[]): AttestationWithScore[] { + return attestationsByScore; + } } interface AttestationWithIndex { diff --git a/packages/beacon-node/src/chain/opPools/attestationPool.ts b/packages/beacon-node/src/chain/opPools/attestationPool.ts index 2fcb36f86f55..38e910753440 100644 --- a/packages/beacon-node/src/chain/opPools/attestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/attestationPool.ts @@ -23,12 +23,16 @@ const SLOTS_RETAINED = 3; */ const MAX_ATTESTATIONS_PER_SLOT = 16_384; -type AggregateFast = { +type AggregateFastPhase0 = { data: allForks.Attestation["data"]; aggregationBits: BitArray; signature: Signature; }; +type AggregateFastElectra = AggregateFastPhase0 & {committeeBits: BitArray}; + +type AggregateFast = AggregateFastPhase0 | AggregateFastElectra; + /** Hex string of DataRoot `TODO` */ type DataRootHex = string; @@ -186,6 +190,26 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: allFork throw Error("Invalid attestation not exactly one bit set"); } + if ("committeeBits" in attestation && !("committeeBits" in aggregate)) { + throw Error("Attempt to aggregate electra attestation into phase0 attestation"); + } + + if (!("committeeBits" in attestation) && "committeeBits" in aggregate) { + throw Error("Attempt to aggregate phase0 attestation into electra attestation"); + } + + if ("committeeBits" in attestation) { + // We assume attestation.committeeBits should already be validated in api and gossip handler and should be non-null + const attestationCommitteeIndex = attestation.committeeBits.getSingleTrueBit(); + const aggregateCommitteeIndex = (aggregate as AggregateFastElectra).committeeBits.getSingleTrueBit(); + + if (attestationCommitteeIndex !== aggregateCommitteeIndex) { + throw Error( + `Committee index mismatched: attestation ${attestationCommitteeIndex} aggregate ${aggregateCommitteeIndex}` + ); + } + } + if (aggregate.aggregationBits.get(bitIndex) === true) { return InsertOutcome.AlreadyKnown; } @@ -202,6 +226,15 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: allFork * Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto() */ function attestationToAggregate(attestation: allForks.Attestation): AggregateFast { + if ("committeeBits" in attestation) { + return { + data: attestation.data, + // clone because it will be mutated + aggregationBits: attestation.aggregationBits.clone(), + committeeBits: attestation.committeeBits, + signature: signatureFromBytesNoCheck(attestation.signature), + }; + } return { data: attestation.data, // clone because it will be mutated @@ -214,9 +247,18 @@ function attestationToAggregate(attestation: allForks.Attestation): AggregateFas * Unwrap AggregateFast to phase0.Attestation */ function fastToAttestation(aggFast: AggregateFast): allForks.Attestation { - return { - data: aggFast.data, - aggregationBits: aggFast.aggregationBits, - signature: aggFast.signature.toBytes(PointFormat.compressed), - }; + if ("committeeBits" in aggFast) { + return { + data: aggFast.data, + aggregationBits: aggFast.aggregationBits, + committeeBits: aggFast.committeeBits, + signature: aggFast.signature.toBytes(PointFormat.compressed), + }; + } else { + return { + data: aggFast.data, + aggregationBits: aggFast.aggregationBits, + signature: aggFast.signature.toBytes(PointFormat.compressed), + }; + } } diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index e33a92e9b00a..71a64c314c7b 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -422,7 +422,11 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler validationResult = await validateGossipAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData); } catch (e) { if (e instanceof AttestationError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(ssz.allForks[fork].SignedAggregateAndProof, signedAggregateAndProof, "gossip_reject"); + chain.persistInvalidSszValue( + ssz.allForks[fork].SignedAggregateAndProof, + signedAggregateAndProof, + "gossip_reject" + ); } throw e; } @@ -451,7 +455,10 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler } } - chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate); + chain.emitter.emit(routes.events.EventType.attestation, { + version: fork, + data: signedAggregateAndProof.message.aggregate, + }); }, [GossipType.beacon_attestation]: async ({ gossipData, @@ -503,7 +510,7 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler } } - chain.emitter.emit(routes.events.EventType.attestation, attestation); + chain.emitter.emit(routes.events.EventType.attestation, {version: fork, data: attestation}); }, [GossipType.attester_slashing]: async ({ @@ -711,7 +718,7 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp } } - chain.emitter.emit(routes.events.EventType.attestation, attestation); + chain.emitter.emit(routes.events.EventType.attestation, {version: fork, data: attestation}); } if (batchableBls) { diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index ee1af9adb75c..cfd5a5b46959 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -43,4 +43,5 @@ export type LightClientFinalityUpdate = ValueOf; export type LightClientStore = ValueOf; +export type AggregateAndProof = ValueOf; export type SignedAggregateAndProof = ValueOf; diff --git a/packages/validator/src/services/attestation.ts b/packages/validator/src/services/attestation.ts index 73162b67f1af..c1eeab5f3e53 100644 --- a/packages/validator/src/services/attestation.ts +++ b/packages/validator/src/services/attestation.ts @@ -1,8 +1,9 @@ import {toHexString} from "@chainsafe/ssz"; -import {BLSSignature, phase0, Slot, ssz} from "@lodestar/types"; +import {allForks, BLSSignature, phase0, Slot, ssz} from "@lodestar/types"; import {computeEpochAtSlot, isAggregatorFromCommitteeLength} from "@lodestar/state-transition"; import {sleep} from "@lodestar/utils"; import {Api, ApiError, routes} from "@lodestar/api"; +import {ChainForkConfig} from "@lodestar/config"; import {IClock, LoggerVc} from "../util/index.js"; import {PubkeyHex} from "../types.js"; import {Metrics} from "../metrics.js"; @@ -41,6 +42,7 @@ export class AttestationService { private readonly emitter: ValidatorEventEmitter, chainHeadTracker: ChainHeaderTracker, private readonly metrics: Metrics | null, + private readonly config: ChainForkConfig, private readonly opts?: AttestationServiceOpts ) { this.dutiesService = new AttestationDutiesService(logger, api, clock, validatorStore, chainHeadTracker, metrics, { @@ -263,7 +265,7 @@ export class AttestationService { const aggregate = res.response; this.metrics?.numParticipantsInAggregate.observe(aggregate.data.aggregationBits.getTrueBitIndexes().length); - const signedAggregateAndProofs: phase0.SignedAggregateAndProof[] = []; + const signedAggregateAndProofs: allForks.SignedAggregateAndProof[] = []; await Promise.all( duties.map(async ({duty, selectionProof}) => { diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 6cd9ed8dc065..1d5d73377946 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -19,6 +19,7 @@ import { DOMAIN_SYNC_COMMITTEE, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, DOMAIN_APPLICATION_BUILDER, + ForkSeq, } from "@lodestar/params"; import { allForks, @@ -26,6 +27,7 @@ import { bellatrix, BLSPubkey, BLSSignature, + electra, Epoch, phase0, Root, @@ -490,7 +492,7 @@ export class ValidatorStore { duty: routes.validator.AttesterDuty, attestationData: phase0.AttestationData, currentEpoch: Epoch - ): Promise { + ): Promise { // Make sure the target epoch is not higher than the current epoch to avoid potential attacks. if (attestationData.target.epoch > currentEpoch) { throw Error( @@ -522,21 +524,30 @@ export class ValidatorStore { data: attestationData, }; - return { - aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), - data: attestationData, - signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), - }; + if (this.config.getForkSeq(duty.slot) >= ForkSeq.electra) { + return { + aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), + data: attestationData, + committeeBits: BitArray.fromSingleBit(duty.committeesAtSlot, duty.committeeIndex), + signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), + } as electra.Attestation; + } else { + return { + aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), + data: attestationData, + signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), + } as phase0.Attestation; + } } async signAggregateAndProof( duty: routes.validator.AttesterDuty, selectionProof: BLSSignature, - aggregate: phase0.Attestation - ): Promise { + aggregate: allForks.Attestation + ): Promise { this.validateAttestationDuty(duty, aggregate.data); - const aggregateAndProof: phase0.AggregateAndProof = { + const aggregateAndProof: allForks.AggregateAndProof = { aggregate, aggregatorIndex: duty.validatorIndex, selectionProof, @@ -544,7 +555,10 @@ export class ValidatorStore { const signingSlot = aggregate.data.slot; const domain = this.config.getDomain(signingSlot, DOMAIN_AGGREGATE_AND_PROOF); - const signingRoot = computeSigningRoot(ssz.phase0.AggregateAndProof, aggregateAndProof, domain); + const signingRoot = + this.config.getForkSeq(duty.slot) >= ForkSeq.electra + ? computeSigningRoot(ssz.electra.AggregateAndProof, aggregateAndProof, domain) + : computeSigningRoot(ssz.phase0.AggregateAndProof, aggregateAndProof, domain); const signableMessage: SignableMessage = { type: SignableMessageType.AGGREGATE_AND_PROOF, @@ -785,6 +799,9 @@ export class ValidatorStore { `Inconsistent duties during signing: duty.committeeIndex ${duty.committeeIndex} != att.committeeIndex ${data.index}` ); } + if (this.config.getForkSeq(duty.slot) >= ForkSeq.electra && data.index !== 0) { + throw Error(`Attestataion data index must be 0 post electra: index ${data.index}`); + } } private assertDoppelgangerSafe(pubKey: PubkeyHex | BLSPubkey): void { diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index 8590fc1d0068..feb4eb340fbd 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -225,6 +225,7 @@ export class Validator { emitter, chainHeaderTracker, metrics, + config, { afterBlockDelaySlotFraction: opts.afterBlockDelaySlotFraction, disableAttestationGrouping: opts.disableAttestationGrouping || opts.distributed, diff --git a/packages/validator/test/unit/services/attestation.test.ts b/packages/validator/test/unit/services/attestation.test.ts index 397fef20b2ba..bb983a8073a6 100644 --- a/packages/validator/test/unit/services/attestation.test.ts +++ b/packages/validator/test/unit/services/attestation.test.ts @@ -3,6 +3,8 @@ import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; import {ssz} from "@lodestar/types"; import {HttpStatusCode, routes} from "@lodestar/api"; +import {createChainForkConfig} from "@lodestar/config"; +import {config} from "@lodestar/config/default"; import {AttestationService, AttestationServiceOpts} from "../../../src/services/attestation.js"; import {AttDutyAndProof} from "../../../src/services/attestationDuties.js"; import {ValidatorStore} from "../../../src/services/validatorStore.js"; @@ -63,6 +65,7 @@ describe("AttestationService", function () { emitter, chainHeadTracker, null, + createChainForkConfig(config), opts );