@lodestar/api
Version:
A Typescript REST client for the Ethereum Consensus API
338 lines (317 loc) • 11.8 kB
text/typescript
import {ContainerType, ListBasicType, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {ForkName, MAX_BLOB_COMMITMENTS_PER_BLOCK} from "@lodestar/params";
import {
Attestation,
AttesterSlashing,
Epoch,
LightClientFinalityUpdate,
LightClientOptimisticUpdate,
RootHex,
SSEPayloadAttributes,
Slot,
StringType,
UintNum64,
altair,
capella,
electra,
phase0,
ssz,
sszTypesFor,
} from "@lodestar/types";
import {EmptyMeta, EmptyResponseCodec, EmptyResponseData} from "../../utils/codecs.js";
import {getPostAltairForkTypes, getPostBellatrixForkTypes} from "../../utils/fork.js";
import {Endpoint, RouteDefinitions, Schema} from "../../utils/index.js";
import {VersionType} from "../../utils/metadata.js";
const stringType = new StringType();
export const blobSidecarSSE = new ContainerType(
{
blockRoot: stringType,
index: ssz.BlobIndex,
slot: ssz.Slot,
kzgCommitment: stringType,
versionedHash: stringType,
},
{typeName: "BlobSidecarSSE", jsonCase: "eth2"}
);
type BlobSidecarSSE = ValueOf<typeof blobSidecarSSE>;
export const dataColumnSidecarSSE = new ContainerType(
{
blockRoot: stringType,
index: ssz.ColumnIndex,
slot: ssz.Slot,
kzgCommitments: new ListBasicType(stringType, MAX_BLOB_COMMITMENTS_PER_BLOCK),
},
{typeName: "DataColumnSidecarSSE", jsonCase: "eth2"}
);
type DataColumnSidecarSSE = ValueOf<typeof dataColumnSidecarSSE>;
export enum EventType {
/**
* The node has finished processing, resulting in a new head. previous_duty_dependent_root is
* `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)` and
* current_duty_dependent_root is `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch) - 1)`.
* Both dependent roots use the genesis block root in the case of underflow.
*/
head = "head",
/** The node has received a block (from P2P or API) that is successfully imported on the fork-choice `on_block` handler */
block = "block",
/** The node has received a block (from P2P or API) that passes validation rules of the `beacon_block` topic */
blockGossip = "block_gossip",
/** The node has received a valid attestation (from P2P or API) */
attestation = "attestation",
/** The node has received a valid SingleAttestation (from P2P or API) */
singleAttestation = "single_attestation",
/** The node has received a valid voluntary exit (from P2P or API) */
voluntaryExit = "voluntary_exit",
/** The node has received a valid proposer slashing (from P2P or API) */
proposerSlashing = "proposer_slashing",
/** The node has received a valid attester slashing (from P2P or API) */
attesterSlashing = "attester_slashing",
/** The node has received a valid blsToExecutionChange (from P2P or API) */
blsToExecutionChange = "bls_to_execution_change",
/** Finalized checkpoint has been updated */
finalizedCheckpoint = "finalized_checkpoint",
/** The node has reorganized its chain */
chainReorg = "chain_reorg",
/** The node has received a valid sync committee SignedContributionAndProof (from P2P or API) */
contributionAndProof = "contribution_and_proof",
/** New or better optimistic header update available */
lightClientOptimisticUpdate = "light_client_optimistic_update",
/** New or better finality update available */
lightClientFinalityUpdate = "light_client_finality_update",
/** Payload attributes for block proposal */
payloadAttributes = "payload_attributes",
/** The node has received a valid BlobSidecar (from P2P or API) */
blobSidecar = "blob_sidecar",
/** The node has received a valid DataColumnSidecar (from P2P or API) */
dataColumnSidecar = "data_column_sidecar",
}
export const eventTypes: {[K in EventType]: K} = {
[]: EventType.head,
[]: EventType.block,
[]: EventType.blockGossip,
[]: EventType.attestation,
[]: EventType.singleAttestation,
[]: EventType.voluntaryExit,
[]: EventType.proposerSlashing,
[]: EventType.attesterSlashing,
[]: EventType.blsToExecutionChange,
[]: EventType.finalizedCheckpoint,
[]: EventType.chainReorg,
[]: EventType.contributionAndProof,
[]: EventType.lightClientOptimisticUpdate,
[]: EventType.lightClientFinalityUpdate,
[]: EventType.payloadAttributes,
[]: EventType.blobSidecar,
[]: EventType.dataColumnSidecar,
};
export type EventData = {
[]: {
slot: Slot;
block: RootHex;
state: RootHex;
epochTransition: boolean;
previousDutyDependentRoot: RootHex;
currentDutyDependentRoot: RootHex;
executionOptimistic: boolean;
};
[]: {
slot: Slot;
block: RootHex;
executionOptimistic: boolean;
};
[]: {
slot: Slot;
block: RootHex;
};
[]: Attestation;
[]: electra.SingleAttestation;
[]: phase0.SignedVoluntaryExit;
[]: phase0.ProposerSlashing;
[]: AttesterSlashing;
[]: capella.SignedBLSToExecutionChange;
[]: {
block: RootHex;
state: RootHex;
epoch: Epoch;
executionOptimistic: boolean;
};
[]: {
slot: Slot;
depth: UintNum64;
oldHeadBlock: RootHex;
newHeadBlock: RootHex;
oldHeadState: RootHex;
newHeadState: RootHex;
epoch: Epoch;
executionOptimistic: boolean;
};
[]: altair.SignedContributionAndProof;
[]: {version: ForkName; data: LightClientOptimisticUpdate};
[]: {version: ForkName; data: LightClientFinalityUpdate};
[]: {version: ForkName; data: SSEPayloadAttributes};
[]: BlobSidecarSSE;
[]: DataColumnSidecarSSE;
};
export type BeaconEvent = {[K in EventType]: {type: K; message: EventData[K]}}[EventType];
type EventstreamArgs = {
/** Event types to subscribe to */
topics: EventType[];
signal: AbortSignal;
onEvent: (event: BeaconEvent) => void;
onError?: (err: Error) => void;
onClose?: () => void;
};
export type Endpoints = {
/**
* Subscribe to beacon node events
* Provides endpoint to subscribe to beacon node Server-Sent-Events stream.
* Consumers should use [eventsource](https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface)
* implementation to listen on those events.
*
* Returns if SSE stream has been opened.
*/
eventstream: Endpoint<
// ⏎
"GET",
EventstreamArgs,
{query: {topics: EventType[]}},
EmptyResponseData,
EmptyMeta
>;
};
export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpoints> {
return {
eventstream: {
url: "/eth/v1/events",
method: "GET",
req: {
writeReq: ({topics}) => ({query: {topics}}),
parseReq: ({query}) => ({topics: query.topics}) as EventstreamArgs,
schema: {
query: {topics: Schema.StringArrayRequired},
},
},
resp: EmptyResponseCodec,
},
};
}
export type TypeJson<T> = {
toJson: (data: T) => unknown; // server
fromJson: (data: unknown) => T; // client
};
export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: TypeJson<EventData[K]>} {
const WithVersion = <T>(getType: (fork: ForkName) => TypeJson<T>): TypeJson<{data: T; version: ForkName}> => {
return {
toJson: ({data, version}) => ({
data: getType(version).toJson(data),
version,
}),
fromJson: (val) => {
const {version} = VersionType.fromJson(val);
return {
data: getType(version).fromJson((val as {data: unknown}).data),
version,
};
},
};
};
return {
[]: new ContainerType(
{
slot: ssz.Slot,
block: stringType,
state: stringType,
epochTransition: ssz.Boolean,
previousDutyDependentRoot: stringType,
currentDutyDependentRoot: stringType,
executionOptimistic: ssz.Boolean,
},
{jsonCase: "eth2"}
),
[]: new ContainerType(
{
slot: ssz.Slot,
block: stringType,
executionOptimistic: ssz.Boolean,
},
{jsonCase: "eth2"}
),
[]: new ContainerType(
{
slot: ssz.Slot,
block: stringType,
},
{jsonCase: "eth2"}
),
[]: {
toJson: (attestation) => {
const fork = config.getForkName(attestation.data.slot);
return sszTypesFor(fork).Attestation.toJson(attestation);
},
fromJson: (attestation) => {
const fork = config.getForkName((attestation as Attestation).data.slot);
return sszTypesFor(fork).Attestation.fromJson(attestation);
},
},
[]: ssz.electra.SingleAttestation,
[]: ssz.phase0.SignedVoluntaryExit,
[]: ssz.phase0.ProposerSlashing,
[]: {
toJson: (attesterSlashing) => {
const fork = config.getForkName(Number(attesterSlashing.attestation1.data.slot));
return sszTypesFor(fork).AttesterSlashing.toJson(attesterSlashing);
},
fromJson: (attesterSlashing) => {
const fork = config.getForkName(Number((attesterSlashing as AttesterSlashing).attestation1.data.slot));
return sszTypesFor(fork).AttesterSlashing.fromJson(attesterSlashing);
},
},
[]: ssz.capella.SignedBLSToExecutionChange,
[]: new ContainerType(
{
block: stringType,
state: stringType,
epoch: ssz.Epoch,
executionOptimistic: ssz.Boolean,
},
{jsonCase: "eth2"}
),
[]: new ContainerType(
{
slot: ssz.Slot,
depth: ssz.UintNum64,
oldHeadBlock: stringType,
newHeadBlock: stringType,
oldHeadState: stringType,
newHeadState: stringType,
epoch: ssz.Epoch,
executionOptimistic: ssz.Boolean,
},
{jsonCase: "eth2"}
),
[]: ssz.altair.SignedContributionAndProof,
[]: WithVersion((fork) => getPostBellatrixForkTypes(fork).SSEPayloadAttributes),
[]: blobSidecarSSE,
[]: dataColumnSidecarSSE,
[]: WithVersion(
(fork) => getPostAltairForkTypes(fork).LightClientOptimisticUpdate
),
[]: WithVersion(
(fork) => getPostAltairForkTypes(fork).LightClientFinalityUpdate
),
};
}
export function getEventSerdes(config: ChainForkConfig) {
const typeByEvent = getTypeByEvent(config);
return {
toJson: (event: BeaconEvent): unknown => {
const eventType = typeByEvent[event.type] as TypeJson<BeaconEvent["message"]>;
return eventType.toJson(event.message);
},
fromJson: (type: EventType, data: unknown): BeaconEvent["message"] => {
const eventType = typeByEvent[type] as TypeJson<BeaconEvent["message"]>;
return eventType.fromJson(data);
},
};
}