Skip to content

Commit

Permalink
feat: add Electra attestation V2 endpoints (#6951)
Browse files Browse the repository at this point in the history
* Initial commit

* getAggregatedAttestationV2

* Lint

* Fix minor flaw

* Add publishAggregateAndProofsV2

* Fix spelling

* Fix CI

* Fix spec test

* Clean up events api

* Run against latest beacon api spec

* Revert changes to emitted events

* Update packages/api/src/beacon/routes/beacon/pool.ts

Co-authored-by: Nico Flaig <[email protected]>

* Update packages/api/src/beacon/routes/beacon/pool.ts

Co-authored-by: Nico Flaig <[email protected]>

* Update packages/api/src/beacon/routes/beacon/pool.ts

Co-authored-by: Nico Flaig <[email protected]>

* Update packages/api/src/beacon/routes/beacon/pool.ts

Co-authored-by: Nico Flaig <[email protected]>

* Address comment

* Add api stub back

* Add todos

* Review PR

* Fix rebase

* Lint

---------

Co-authored-by: Nico Flaig <[email protected]>
  • Loading branch information
2 people authored and g11tech committed Aug 9, 2024
1 parent bb9d811 commit f2e3fa6
Show file tree
Hide file tree
Showing 21 changed files with 448 additions and 179 deletions.
27 changes: 24 additions & 3 deletions packages/api/src/beacon/routes/beacon/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {
deneb,
isSignedBlockContents,
SignedBeaconBlock,
BeaconBlockBody,
SignedBeaconBlockOrContents,
SignedBlindedBeaconBlock,
SignedBlockContents,
sszTypesFor,
BeaconBlockBody,
} from "@lodestar/types";
import {ForkName, ForkPreExecution, isForkBlobs, isForkExecution} from "@lodestar/params";
import {ForkName, ForkPreElectra, ForkPreExecution, isForkBlobs, isForkExecution} from "@lodestar/params";
import {Endpoint, RequestCodec, RouteDefinitions, Schema} from "../../../utils/index.js";
import {EmptyMeta, EmptyResponseCodec, EmptyResponseData, WithVersion} from "../../../utils/codecs.js";
import {
Expand Down Expand Up @@ -101,10 +101,22 @@ export type Endpoints = {
"GET",
BlockArgs,
{params: {block_id: string}},
BeaconBlockBody["attestations"],
BeaconBlockBody<ForkPreElectra>["attestations"],
ExecutionOptimisticAndFinalizedMeta
>;

/**
* Get block attestations
* Retrieves attestation included in requested block.
*/
getBlockAttestationsV2: Endpoint<
"GET",
BlockArgs,
{params: {block_id: string}},
BeaconBlockBody["attestations"],
ExecutionOptimisticFinalizedAndVersionMeta
>;

/**
* Get block header
* Retrieves block header for given block id.
Expand Down Expand Up @@ -251,6 +263,15 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
meta: ExecutionOptimisticAndFinalizedCodec,
},
},
getBlockAttestationsV2: {
url: "/eth/v2/beacon/blocks/{block_id}/attestations",
method: "GET",
req: blockIdOnlyReq,
resp: {
data: WithVersion((fork) => ssz[fork].BeaconBlockBody.fields.attestations),
meta: ExecutionOptimisticFinalizedAndVersionCodec,
},
},
getBlockHeader: {
url: "/eth/v1/beacon/headers/{block_id}",
method: "GET",
Expand Down
201 changes: 166 additions & 35 deletions packages/api/src/beacon/routes/beacon/pool.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* 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 {isForkElectra} from "@lodestar/params";
import {phase0, capella, CommitteeIndex, Slot, ssz, electra, AttesterSlashing} from "@lodestar/types";
import {Schema, Endpoint, RouteDefinitions} from "../../../utils/index.js";
import {
ArrayOf,
Expand All @@ -23,7 +23,8 @@ import {fromHeaders} from "../../../utils/headers.js";

const AttestationListTypePhase0 = ArrayOf(ssz.phase0.Attestation);
const AttestationListTypeElectra = ArrayOf(ssz.electra.Attestation);
const AttesterSlashingListType = ArrayOf(ssz.phase0.AttesterSlashing);
const AttesterSlashingListTypePhase0 = ArrayOf(ssz.phase0.AttesterSlashing);
const AttesterSlashingListTypeElectra = ArrayOf(ssz.electra.AttesterSlashing);
const ProposerSlashingListType = ArrayOf(ssz.phase0.ProposerSlashing);
const SignedVoluntaryExitListType = ArrayOf(ssz.phase0.SignedVoluntaryExit);
const SignedBLSToExecutionChangeListType = ArrayOf(ssz.capella.SignedBLSToExecutionChange);
Expand All @@ -32,7 +33,11 @@ const SyncCommitteeMessageListType = ArrayOf(ssz.altair.SyncCommitteeMessage);
type AttestationListPhase0 = ValueOf<typeof AttestationListTypePhase0>;
type AttestationListElectra = ValueOf<typeof AttestationListTypeElectra>;
type AttestationList = AttestationListPhase0 | AttestationListElectra;
type AttesterSlashingList = ValueOf<typeof AttesterSlashingListType>;

type AttesterSlashingListPhase0 = ValueOf<typeof AttesterSlashingListTypePhase0>;
type AttesterSlashingListElectra = ValueOf<typeof AttesterSlashingListTypeElectra>;
type AttesterSlashingList = AttesterSlashingListPhase0 | AttesterSlashingListElectra;

type ProposerSlashingList = ValueOf<typeof ProposerSlashingListType>;
type SignedVoluntaryExitList = ValueOf<typeof SignedVoluntaryExitListType>;
type SignedBLSToExecutionChangeList = ValueOf<typeof SignedBLSToExecutionChangeListType>;
Expand All @@ -44,6 +49,18 @@ export type Endpoints = {
* Retrieves attestations known by the node but not necessarily incorporated into any block
*/
getPoolAttestations: Endpoint<
"GET",
{slot?: Slot; committeeIndex?: CommitteeIndex},
{query: {slot?: number; committee_index?: number}},
AttestationListPhase0,
EmptyMeta
>;

/**
* Get Attestations from operations pool
* Retrieves attestations known by the node but not necessarily incorporated into any block
*/
getPoolAttestationsV2: Endpoint<
"GET",
{slot?: Slot; committeeIndex?: CommitteeIndex},
{query: {slot?: number; committee_index?: number}},
Expand All @@ -60,10 +77,23 @@ export type Endpoints = {
"GET",
EmptyArgs,
EmptyRequest,
AttesterSlashingList,
AttesterSlashingListPhase0,
EmptyMeta
>;

/**
* Get AttesterSlashings from operations pool
* Retrieves attester slashings known by the node but not necessarily incorporated into any block
*/
getPoolAttesterSlashingsV2: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
AttesterSlashingList,
VersionMeta
>;

/**
* Get ProposerSlashings from operations pool
* Retrieves proposer slashings known by the node but not necessarily incorporated into any block
Expand Down Expand Up @@ -112,6 +142,22 @@ export type Endpoints = {
* If one or more attestations fail validation the node MUST return a 400 error with details of which attestations have failed, and why.
*/
submitPoolAttestations: Endpoint<
"POST",
{signedAttestations: AttestationListPhase0},
{body: unknown},
EmptyResponseData,
EmptyMeta
>;

/**
* Submit Attestation objects to node
* Submits Attestation objects to the node. Each attestation in the request body is processed individually.
*
* If an attestation is validated successfully the node MUST publish that attestation on the appropriate subnet.
*
* If one or more attestations fail validation the node MUST return a 400 error with details of which attestations have failed, and why.
*/
submitPoolAttestationsV2: Endpoint<
"POST",
{signedAttestations: AttestationList},
{body: unknown; headers: {[MetaHeader.Version]: string}},
Expand All @@ -131,6 +177,18 @@ export type Endpoints = {
EmptyMeta
>;

/**
* Submit AttesterSlashing object to node's pool
* Submits AttesterSlashing object to node's pool and if passes validation node MUST broadcast it to network.
*/
submitPoolAttesterSlashingsV2: Endpoint<
"POST",
{attesterSlashing: AttesterSlashing},
{body: unknown; headers: {[MetaHeader.Version]: string}},
EmptyResponseData,
EmptyMeta
>;

/**
* Submit ProposerSlashing object to node's pool
* Submits ProposerSlashing object to node's pool and if passes validation node MUST broadcast it to network.
Expand Down Expand Up @@ -191,9 +249,20 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
schema: {query: {slot: Schema.Uint, committee_index: Schema.Uint}},
},
resp: {
data: WithVersion((fork) =>
ForkSeq[fork] >= ForkSeq.electra ? AttestationListTypeElectra : AttestationListTypePhase0
),
data: AttestationListTypePhase0,
meta: EmptyMetaCodec,
},
},
getPoolAttestationsV2: {
url: "/eth/v2/beacon/pool/attestations",
method: "GET",
req: {
writeReq: ({slot, committeeIndex}) => ({query: {slot, committee_index: committeeIndex}}),
parseReq: ({query}) => ({slot: query.slot, committeeIndex: query.committee_index}),
schema: {query: {slot: Schema.Uint, committee_index: Schema.Uint}},
},
resp: {
data: WithVersion((fork) => (isForkElectra(fork) ? AttestationListTypeElectra : AttestationListTypePhase0)),
meta: VersionCodec,
},
},
Expand All @@ -202,10 +271,21 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
method: "GET",
req: EmptyRequestCodec,
resp: {
data: AttesterSlashingListType,
data: AttesterSlashingListTypePhase0,
meta: EmptyMetaCodec,
},
},
getPoolAttesterSlashingsV2: {
url: "/eth/v2/beacon/pool/attester_slashings",
method: "GET",
req: EmptyRequestCodec,
resp: {
data: WithVersion((fork) =>
isForkElectra(fork) ? AttesterSlashingListTypeElectra : AttesterSlashingListTypePhase0
),
meta: VersionCodec,
},
},
getPoolProposerSlashings: {
url: "/eth/v1/beacon/pool/proposer_slashings",
method: "GET",
Expand Down Expand Up @@ -236,49 +316,55 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
submitPoolAttestations: {
url: "/eth/v1/beacon/pool/attestations",
method: "POST",
req: {
writeReqJson: ({signedAttestations}) => ({
body: AttestationListTypePhase0.toJson(signedAttestations),
}),
parseReqJson: ({body}) => ({signedAttestations: AttestationListTypePhase0.fromJson(body)}),
writeReqSsz: ({signedAttestations}) => ({body: AttestationListTypePhase0.serialize(signedAttestations)}),
parseReqSsz: ({body}) => ({signedAttestations: AttestationListTypePhase0.deserialize(body)}),
schema: {
body: Schema.ObjectArray,
},
},
resp: EmptyResponseCodec,
},
submitPoolAttestationsV2: {
url: "/eth/v2/beacon/pool/attestations",
method: "POST",
req: {
writeReqJson: ({signedAttestations}) => {
const fork = config.getForkName(signedAttestations[0].data.slot);
const fork = config.getForkName(signedAttestations[0]?.data.slot ?? 0);
return {
body:
ForkSeq[fork] >= ForkSeq.electra
? AttestationListTypeElectra.toJson(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.toJson(signedAttestations as AttestationListPhase0),
body: isForkElectra(fork)
? AttestationListTypeElectra.toJson(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.toJson(signedAttestations as AttestationListPhase0),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqJson: ({body, headers}) => {
const versionHeader = fromHeaders(headers, MetaHeader.Version, false);
const fork =
versionHeader !== undefined
? toForkName(versionHeader)
: config.getForkName(Number((body as {data: {slot: string}}[])[0]?.data.slot ?? 0));

const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedAttestations:
ForkSeq[fork] >= ForkSeq.electra
? AttestationListTypeElectra.fromJson(body)
: AttestationListTypePhase0.fromJson(body),
signedAttestations: isForkElectra(fork)
? AttestationListTypeElectra.fromJson(body)
: AttestationListTypePhase0.fromJson(body),
};
},
writeReqSsz: ({signedAttestations}) => {
const fork = config.getForkName(signedAttestations[0].data.slot);
const fork = config.getForkName(signedAttestations[0]?.data.slot ?? 0);
return {
body:
ForkSeq[fork] >= ForkSeq.electra
? AttestationListTypeElectra.serialize(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.serialize(signedAttestations as AttestationListPhase0),
body: isForkElectra(fork)
? AttestationListTypeElectra.serialize(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.serialize(signedAttestations as AttestationListPhase0),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqSsz: ({body, headers}) => {
const versionHeader = fromHeaders(headers, MetaHeader.Version, true);
const fork = toForkName(versionHeader);
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedAttestations:
ForkSeq[fork] >= ForkSeq.electra
? AttestationListTypeElectra.deserialize(body)
: AttestationListTypePhase0.deserialize(body),
signedAttestations: isForkElectra(fork)
? AttestationListTypeElectra.deserialize(body)
: AttestationListTypePhase0.deserialize(body),
};
},
schema: {
Expand All @@ -302,6 +388,51 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
},
resp: EmptyResponseCodec,
},
submitPoolAttesterSlashingsV2: {
url: "/eth/v2/beacon/pool/attester_slashings",
method: "POST",
req: {
writeReqJson: ({attesterSlashing}) => {
const fork = config.getForkName(Number(attesterSlashing.attestation1.data.slot));
return {
body: isForkElectra(fork)
? ssz.electra.AttesterSlashing.toJson(attesterSlashing)
: ssz.phase0.AttesterSlashing.toJson(attesterSlashing),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqJson: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
attesterSlashing: isForkElectra(fork)
? ssz.electra.AttesterSlashing.fromJson(body)
: ssz.phase0.AttesterSlashing.fromJson(body),
};
},
writeReqSsz: ({attesterSlashing}) => {
const fork = config.getForkName(Number(attesterSlashing.attestation1.data.slot));
return {
body: isForkElectra(fork)
? ssz.electra.AttesterSlashing.serialize(attesterSlashing as electra.AttesterSlashing)
: ssz.electra.AttesterSlashing.serialize(attesterSlashing as phase0.AttesterSlashing),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqSsz: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
attesterSlashing: isForkElectra(fork)
? ssz.electra.AttesterSlashing.deserialize(body)
: ssz.phase0.AttesterSlashing.deserialize(body),
};
},
schema: {
body: Schema.Object,
headers: {[MetaHeader.Version]: Schema.String},
},
},
resp: EmptyResponseCodec,
},
submitPoolProposerSlashings: {
url: "/eth/v1/beacon/pool/proposer_slashings",
method: "POST",
Expand Down
11 changes: 4 additions & 7 deletions packages/api/src/beacon/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import {
LightClientOptimisticUpdate,
LightClientFinalityUpdate,
SSEPayloadAttributes,
Attestation,
AttesterSlashing,
sszTypesFor,
} from "@lodestar/types";
import {ForkName} from "@lodestar/params";

Expand Down Expand Up @@ -107,10 +104,10 @@ export type EventData = {
block: RootHex;
executionOptimistic: boolean;
};
[EventType.attestation]: {version: ForkName; data: Attestation};
[EventType.attestation]: phase0.Attestation;
[EventType.voluntaryExit]: phase0.SignedVoluntaryExit;
[EventType.proposerSlashing]: phase0.ProposerSlashing;
[EventType.attesterSlashing]: {version: ForkName; data: AttesterSlashing};
[EventType.attesterSlashing]: phase0.AttesterSlashing;
[EventType.blsToExecutionChange]: capella.SignedBLSToExecutionChange;
[EventType.finalizedCheckpoint]: {
block: RootHex;
Expand Down Expand Up @@ -228,10 +225,10 @@ export function getTypeByEvent(): {[K in EventType]: TypeJson<EventData[K]>} {
{jsonCase: "eth2"}
),

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

[EventType.finalizedCheckpoint]: new ContainerType(
Expand Down
Loading

0 comments on commit f2e3fa6

Please sign in to comment.