Skip to content

Commit

Permalink
feat: implement EIP-7549 (#6689)
Browse files Browse the repository at this point in the history
* initial commit

* lint

* Add getAttestingIndices and update getIndexedAttestation

* Update gossip validation

* Update attestation gossip validation

* aggregateAndProof validation

* clean up

* Validator

* Misc

* Fix the build erros

* feat: get attestations for electra block (#6732)

* feat: getAttestationsForBlock() for electra

* chore: fix lint

* fix: MAX_ATTESTATIONS_PER_GROUP_ELECTRA and address PR comments

* chore: unit test aggregateConsolidation

* Fix rebase mistake

* Address my own comment :)

---------

Co-authored-by: Navie Chan <naviechan@gmail.com>

* Fix check-types

* Address comments

---------

Co-authored-by: Nazar Hussain <nazarhussain@gmail.com>
Co-authored-by: tuyennhv <tuyen@chainsafe.io>
Co-authored-by: gajinder <develop@g11tech.io>
  • Loading branch information
4 people committed Jul 31, 2024
1 parent 6bd9dd0 commit 4a8289b
Show file tree
Hide file tree
Showing 28 changed files with 718 additions and 191 deletions.
66 changes: 55 additions & 11 deletions packages/api/src/beacon/routes/beacon/pool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {ForkSeq} from "@lodestar/params";
import {phase0, capella, CommitteeIndex, Slot, ssz} from "@lodestar/types";
import {Schema, Endpoint, RouteDefinitions} from "../../../utils/index.js";
import {
Expand All @@ -12,18 +13,24 @@ import {
EmptyRequest,
EmptyResponseCodec,
EmptyResponseData,
WithVersion,
} from "../../../utils/codecs.js";
import {MetaHeader, VersionCodec, VersionMeta} from "../../../utils/metadata.js";
import {toForkName} from "../../../utils/fork.js";

// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes

const AttestationListType = ArrayOf(ssz.phase0.Attestation);
const AttestationListTypePhase0 = ArrayOf(ssz.phase0.Attestation);
const AttestationListTypeElectra = ArrayOf(ssz.electra.Attestation);
const AttesterSlashingListType = ArrayOf(ssz.phase0.AttesterSlashing);
const ProposerSlashingListType = ArrayOf(ssz.phase0.ProposerSlashing);
const SignedVoluntaryExitListType = ArrayOf(ssz.phase0.SignedVoluntaryExit);
const SignedBLSToExecutionChangeListType = ArrayOf(ssz.capella.SignedBLSToExecutionChange);
const SyncCommitteeMessageListType = ArrayOf(ssz.altair.SyncCommitteeMessage);

type AttestationList = ValueOf<typeof AttestationListType>;
type AttestationListPhase0 = ValueOf<typeof AttestationListTypePhase0>;
type AttestationListElectra = ValueOf<typeof AttestationListTypeElectra>;
type AttestationList = AttestationListPhase0 | AttestationListElectra;
type AttesterSlashingList = ValueOf<typeof AttesterSlashingListType>;
type ProposerSlashingList = ValueOf<typeof ProposerSlashingListType>;
type SignedVoluntaryExitList = ValueOf<typeof SignedVoluntaryExitListType>;
Expand All @@ -40,7 +47,7 @@ export type Endpoints = {
{slot?: Slot; committeeIndex?: CommitteeIndex},
{query: {slot?: number; committee_index?: number}},
AttestationList,
EmptyMeta
VersionMeta
>;

/**
Expand Down Expand Up @@ -106,7 +113,7 @@ export type Endpoints = {
submitPoolAttestations: Endpoint<
"POST",
{signedAttestations: AttestationList},
{body: unknown},
{body: unknown; headers: {[MetaHeader.Version]: string}},
EmptyResponseData,
EmptyMeta
>;
Expand Down Expand Up @@ -172,7 +179,7 @@ export type Endpoints = {
>;
};

export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpoints> {
export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoints> {
return {
getPoolAttestations: {
url: "/eth/v1/beacon/pool/attestations",
Expand All @@ -183,8 +190,10 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
schema: {query: {slot: Schema.Uint, committee_index: Schema.Uint}},
},
resp: {
data: AttestationListType,
meta: EmptyMetaCodec,
data: WithVersion((fork) =>
ForkSeq[fork] >= ForkSeq.electra ? AttestationListTypeElectra : AttestationListTypePhase0
),
meta: VersionCodec,
},
},
getPoolAttesterSlashings: {
Expand Down Expand Up @@ -227,12 +236,47 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
url: "/eth/v1/beacon/pool/attestations",
method: "POST",
req: {
writeReqJson: ({signedAttestations}) => ({body: AttestationListType.toJson(signedAttestations)}),
parseReqJson: ({body}) => ({signedAttestations: AttestationListType.fromJson(body)}),
writeReqSsz: ({signedAttestations}) => ({body: AttestationListType.serialize(signedAttestations)}),
parseReqSsz: ({body}) => ({signedAttestations: AttestationListType.deserialize(body)}),
writeReqJson: ({signedAttestations}) => {
const fork = config.getForkName(signedAttestations[0].data.slot);
return {
body:
ForkSeq[fork] >= ForkSeq.electra
? AttestationListTypeElectra.toJson(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.toJson(signedAttestations as AttestationListPhase0),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqJson: ({body, headers}) => {
const fork = toForkName(headers[MetaHeader.Version]);
return {
signedAttestations:
ForkSeq[fork] >= ForkSeq.electra
? AttestationListTypeElectra.fromJson(body)
: AttestationListTypePhase0.fromJson(body),
};
},
writeReqSsz: ({signedAttestations}) => {
const fork = config.getForkName(signedAttestations[0].data.slot);
return {
body:
ForkSeq[fork] >= ForkSeq.electra
? AttestationListTypeElectra.serialize(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.serialize(signedAttestations as AttestationListPhase0),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqSsz: ({body, headers}) => {
const fork = toForkName(headers[MetaHeader.Version]);
return {
signedAttestations:
ForkSeq[fork] >= ForkSeq.electra
? AttestationListTypeElectra.deserialize(body)
: AttestationListTypePhase0.deserialize(body),
};
},
schema: {
body: Schema.ObjectArray,
headers: {[MetaHeader.Version]: Schema.String},
},
},
resp: EmptyResponseCodec,
Expand Down
8 changes: 4 additions & 4 deletions packages/api/src/beacon/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ export type EventData = {
block: RootHex;
executionOptimistic: boolean;
};
[EventType.attestation]: phase0.Attestation;
[EventType.attestation]: {version: ForkName; data: allForks.Attestation};
[EventType.voluntaryExit]: phase0.SignedVoluntaryExit;
[EventType.proposerSlashing]: phase0.ProposerSlashing;
[EventType.attesterSlashing]: phase0.AttesterSlashing;
[EventType.attesterSlashing]: {version: ForkName; data: allForks.AttesterSlashing};
[EventType.blsToExecutionChange]: capella.SignedBLSToExecutionChange;
[EventType.finalizedCheckpoint]: {
block: RootHex;
Expand Down Expand Up @@ -225,10 +225,10 @@ export function getTypeByEvent(): {[K in EventType]: TypeJson<EventData[K]>} {
{jsonCase: "eth2"}
),

[EventType.attestation]: ssz.phase0.Attestation,
[EventType.attestation]: WithVersion((fork) => (ssz.allForks[fork] as allForks.AllForksSSZTypes).Attestation),
[EventType.voluntaryExit]: ssz.phase0.SignedVoluntaryExit,
[EventType.proposerSlashing]: ssz.phase0.ProposerSlashing,
[EventType.attesterSlashing]: ssz.phase0.AttesterSlashing,
[EventType.attesterSlashing]: WithVersion((fork) => ssz.allForks[fork].AttesterSlashing),
[EventType.blsToExecutionChange]: ssz.capella.SignedBLSToExecutionChange,

[EventType.finalizedCheckpoint]: new ContainerType(
Expand Down
93 changes: 71 additions & 22 deletions packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {ContainerType, fromHexString, toHexString, Type, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {isForkBlobs} from "@lodestar/params";
import {isForkBlobs, ForkSeq} from "@lodestar/params";
import {
altair,
BLSSignature,
Expand Down Expand Up @@ -208,7 +208,8 @@ export const ValidatorIndicesType = ArrayOf(ssz.ValidatorIndex);
export const AttesterDutyListType = ArrayOf(AttesterDutyType);
export const ProposerDutyListType = ArrayOf(ProposerDutyType);
export const SyncDutyListType = ArrayOf(SyncDutyType);
export const SignedAggregateAndProofListType = ArrayOf(ssz.phase0.SignedAggregateAndProof);
export const SignedAggregateAndProofListPhase0Type = ArrayOf(ssz.phase0.SignedAggregateAndProof);
export const SignedAggregateAndProofListElectaType = ArrayOf(ssz.electra.SignedAggregateAndProof);
export const SignedContributionAndProofListType = ArrayOf(ssz.altair.SignedContributionAndProof);
export const BeaconCommitteeSubscriptionListType = ArrayOf(BeaconCommitteeSubscriptionType);
export const SyncCommitteeSubscriptionListType = ArrayOf(SyncCommitteeSubscriptionType);
Expand All @@ -225,7 +226,9 @@ export type ProposerDuty = ValueOf<typeof ProposerDutyType>;
export type ProposerDutyList = ValueOf<typeof ProposerDutyListType>;
export type SyncDuty = ValueOf<typeof SyncDutyType>;
export type SyncDutyList = ValueOf<typeof SyncDutyListType>;
export type SignedAggregateAndProofList = ValueOf<typeof SignedAggregateAndProofListType>;
export type SignedAggregateAndProofListPhase0 = ValueOf<typeof SignedAggregateAndProofListPhase0Type>;
export type SignedAggregateAndProofListElecta = ValueOf<typeof SignedAggregateAndProofListElectaType>;
export type SignedAggregateAndProofList = SignedAggregateAndProofListPhase0 | SignedAggregateAndProofListElecta;
export type SignedContributionAndProofList = ValueOf<typeof SignedContributionAndProofListType>;
export type BeaconCommitteeSubscription = ValueOf<typeof BeaconCommitteeSubscriptionType>;
export type BeaconCommitteeSubscriptionList = ValueOf<typeof BeaconCommitteeSubscriptionListType>;
Expand Down Expand Up @@ -406,10 +409,11 @@ export type Endpoints = {
/** HashTreeRoot of AttestationData that validator want's aggregated */
attestationDataRoot: Root;
slot: Slot;
index: number;
},
{query: {attestation_data_root: string; slot: number}},
phase0.Attestation,
EmptyMeta
{query: {attestation_data_root: string; slot: number; index: number}},
allForks.Attestation,
VersionMeta
>;

/**
Expand All @@ -419,7 +423,7 @@ export type Endpoints = {
publishAggregateAndProofs: Endpoint<
"POST",
{signedAggregateAndProofs: SignedAggregateAndProofList},
{body: unknown},
{body: unknown; headers: {[MetaHeader.Version]: string}},
EmptyResponseData,
EmptyMeta
>;
Expand Down Expand Up @@ -536,7 +540,7 @@ export type Endpoints = {
>;
};

export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpoints> {
export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoints> {
return {
getAttesterDuties: {
url: "/eth/v1/validator/duties/attester/{epoch}",
Expand Down Expand Up @@ -801,33 +805,78 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
url: "/eth/v1/validator/aggregate_attestation",
method: "GET",
req: {
writeReq: ({attestationDataRoot, slot}) => ({
query: {attestation_data_root: toHexString(attestationDataRoot), slot},
writeReq: ({attestationDataRoot, slot, index}) => ({
query: {attestation_data_root: toHexString(attestationDataRoot), slot, index},
}),
parseReq: ({query}) => ({
attestationDataRoot: fromHexString(query.attestation_data_root),
slot: query.slot,
index: query.slot,
}),
parseReq: ({query}) => ({attestationDataRoot: fromHexString(query.attestation_data_root), slot: query.slot}),
schema: {
query: {attestation_data_root: Schema.StringRequired, slot: Schema.UintRequired},
query: {attestation_data_root: Schema.StringRequired, slot: Schema.UintRequired, index: Schema.UintRequired},
},
},
resp: {
data: ssz.phase0.Attestation,
meta: EmptyMetaCodec,
data: WithVersion((fork) =>
ForkSeq[fork] >= ForkSeq.electra ? ssz.electra.Attestation : ssz.phase0.Attestation
),
meta: VersionCodec,
},
},
publishAggregateAndProofs: {
url: "/eth/v1/validator/aggregate_and_proofs",
method: "POST",
req: {
writeReqJson: ({signedAggregateAndProofs}) => ({
body: SignedAggregateAndProofListType.toJson(signedAggregateAndProofs),
}),
parseReqJson: ({body}) => ({signedAggregateAndProofs: SignedAggregateAndProofListType.fromJson(body)}),
writeReqSsz: ({signedAggregateAndProofs}) => ({
body: SignedAggregateAndProofListType.serialize(signedAggregateAndProofs),
}),
parseReqSsz: ({body}) => ({signedAggregateAndProofs: SignedAggregateAndProofListType.deserialize(body)}),
writeReqJson: ({signedAggregateAndProofs}) => {
const fork = config.getForkName(signedAggregateAndProofs[0].message.aggregate.data.slot);
return {
body:
ForkSeq[fork] >= ForkSeq.electra
? SignedAggregateAndProofListElectaType.toJson(
signedAggregateAndProofs as SignedAggregateAndProofListElecta
)
: SignedAggregateAndProofListPhase0Type.toJson(
signedAggregateAndProofs as SignedAggregateAndProofListPhase0
),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqJson: ({body, headers}) => {
const fork = toForkName(headers[MetaHeader.Version]);
return {
signedAggregateAndProofs:
ForkSeq[fork] >= ForkSeq.electra
? SignedAggregateAndProofListElectaType.fromJson(body)
: SignedAggregateAndProofListPhase0Type.fromJson(body),
};
},
writeReqSsz: ({signedAggregateAndProofs}) => {
const fork = config.getForkName(signedAggregateAndProofs[0].message.aggregate.data.slot);
return {
body:
ForkSeq[fork] >= ForkSeq.electra
? SignedAggregateAndProofListElectaType.serialize(
signedAggregateAndProofs as SignedAggregateAndProofListElecta
)
: SignedAggregateAndProofListPhase0Type.serialize(
signedAggregateAndProofs as SignedAggregateAndProofListPhase0
),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqSsz: ({body, headers}) => {
const fork = toForkName(headers[MetaHeader.Version]);
return {
signedAggregateAndProofs:
ForkSeq[fork] >= ForkSeq.electra
? SignedAggregateAndProofListElectaType.deserialize(body)
: SignedAggregateAndProofListPhase0Type.deserialize(body),
};
},
schema: {
body: Schema.ObjectArray,
headers: {[MetaHeader.Version]: Schema.String},
},
},
resp: EmptyResponseCodec,
Expand Down
4 changes: 2 additions & 2 deletions packages/beacon-node/src/api/impl/beacon/pool/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {routes} from "@lodestar/api";
import {ApplicationMethods} from "@lodestar/api/server";
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";
Expand Down Expand Up @@ -78,7 +78,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);
Expand Down
4 changes: 3 additions & 1 deletion packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,7 @@ export function getValidatorApi(
const attEpoch = computeEpochAtSlot(slot);
const headBlockRootHex = chain.forkChoice.getHead().blockRoot;
const headBlockRoot = fromHex(headBlockRootHex);
const fork = config.getForkSeq(slot);

const beaconBlockRoot =
slot >= headSlot
Expand Down Expand Up @@ -845,7 +846,7 @@ export function getValidatorApi(
return {
data: {
slot,
index: committeeIndex,
index: fork >= ForkSeq.electra ? 0 : committeeIndex,
beaconBlockRoot,
source: attEpochState.currentJustifiedCheckpoint,
target: {epoch: attEpoch, root: targetRoot},
Expand Down Expand Up @@ -1082,6 +1083,7 @@ export function getValidatorApi(

return {
data: aggregate,
version: config.getForkName(slot),
};
},

Expand Down
10 changes: 8 additions & 2 deletions packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,12 +428,18 @@ 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)) {
for (const attesterSlashing of block.message.body.attesterSlashings) {
this.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing);
this.emitter.emit(routes.events.EventType.attesterSlashing, {
version: this.config.getForkName(blockSlot),
data: attesterSlashing,
});
}
}
if (this.emitter.listenerCount(routes.events.EventType.proposerSlashing)) {
Expand Down
12 changes: 11 additions & 1 deletion packages/beacon-node/src/chain/errors/attestationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ export enum AttestationErrorCode {
INVALID_SERIALIZED_BYTES = "ATTESTATION_ERROR_INVALID_SERIALIZED_BYTES",
/** Too many skipped slots. */
TOO_MANY_SKIPPED_SLOTS = "ATTESTATION_ERROR_TOO_MANY_SKIPPED_SLOTS",
/**
* Electra: The aggregated attestation doesn't have only one committee bit set.
*/
NOT_EXACTLY_ONE_COMMITTEE_BIT_SET = "ATTESTATION_ERROR_NOT_EXACTLY_ONE_COMMITTEE_BIT_SET",
/**
* Electra: Invalid attestationData index: is non-zero
*/
NON_ZERO_ATTESTATION_DATA_INDEX = "ATTESTATION_ERROR_NON_ZERO_ATTESTATION_DATA_INDEX",
}

export type AttestationErrorType =
Expand Down Expand Up @@ -160,7 +168,9 @@ export type AttestationErrorType =
| {code: AttestationErrorCode.INVALID_AGGREGATOR}
| {code: AttestationErrorCode.INVALID_INDEXED_ATTESTATION}
| {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES}
| {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot};
| {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot}
| {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}
| {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX};

export class AttestationError extends GossipActionError<AttestationErrorType> {
getMetadata(): Record<string, string | number | null> {
Expand Down
Loading

0 comments on commit 4a8289b

Please sign in to comment.