@lodestar/api
Version:
A Typescript REST client for the Ethereum Consensus API
242 lines (232 loc) • 8.77 kB
text/typescript
import {ListCompositeType, ValueOf} from "@chainsafe/ssz";
import {BeaconConfig, ChainForkConfig, createBeaconConfig} from "@lodestar/config";
import {NetworkName, genesisData} from "@lodestar/config/networks";
import {ForkName, SLOTS_PER_EPOCH, ZERO_HASH} from "@lodestar/params";
import {
LightClientBootstrap,
LightClientFinalityUpdate,
LightClientOptimisticUpdate,
LightClientUpdate,
SyncPeriod,
ssz,
} from "@lodestar/types";
import {fromHex} from "@lodestar/utils";
import {
EmptyArgs,
EmptyMeta,
EmptyMetaCodec,
EmptyRequest,
EmptyRequestCodec,
WithVersion,
} from "../../utils/codecs.js";
import {getPostAltairForkTypes, toForkName} from "../../utils/fork.js";
import {Endpoint, RouteDefinitions, Schema} from "../../utils/index.js";
import {MetaHeader, VersionCodec, VersionMeta} from "../../utils/metadata.js";
export const HashListType = new ListCompositeType(ssz.Root, 10000);
export type HashList = ValueOf<typeof HashListType>;
export type Endpoints = {
/**
* Returns an array of best updates given a `startPeriod` and `count` number of sync committee period to return.
* Best is defined by (in order of priority):
* - Is finalized update
* - Has most bits
* - Oldest update
*/
getLightClientUpdatesByRange: Endpoint<
"GET",
{startPeriod: SyncPeriod; count: number},
{query: {start_period: number; count: number}},
LightClientUpdate[],
{versions: ForkName[]}
>;
/**
* Returns the latest optimistic head update available. Clients should use the SSE type `light_client_optimistic_update`
* unless to get the very first head update after syncing, or if SSE are not supported by the server.
*/
getLightClientOptimisticUpdate: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
LightClientOptimisticUpdate,
VersionMeta
>;
getLightClientFinalityUpdate: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
LightClientFinalityUpdate,
VersionMeta
>;
/**
* Fetch a bootstrapping state with a proof to a trusted block root.
* The trusted block root should be fetched with similar means to a weak subjectivity checkpoint.
* Only block roots for checkpoints are guaranteed to be available.
*/
getLightClientBootstrap: Endpoint<
"GET",
{blockRoot: string},
{params: {block_root: string}},
LightClientBootstrap,
VersionMeta
>;
/**
* Returns an array of sync committee hashes based on the provided period and count
*/
getLightClientCommitteeRoot: Endpoint<
"GET",
{startPeriod: SyncPeriod; count: number},
{query: {start_period: number; count: number}},
HashList,
EmptyMeta
>;
};
export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoints> {
// Cache config so fork digests don't need to be recomputed
let beaconConfig: BeaconConfig | undefined;
const cachedBeaconConfig = (): BeaconConfig => {
if (beaconConfig === undefined) {
const genesisValidatorsRoot = genesisData[config.CONFIG_NAME as NetworkName]?.genesisValidatorsRoot;
beaconConfig = createBeaconConfig(config, genesisValidatorsRoot ? fromHex(genesisValidatorsRoot) : ZERO_HASH);
}
return beaconConfig;
};
return {
getLightClientUpdatesByRange: {
url: "/eth/v1/beacon/light_client/updates",
method: "GET",
req: {
writeReq: ({startPeriod, count}) => ({query: {start_period: startPeriod, count}}),
parseReq: ({query}) => ({startPeriod: query.start_period, count: query.count}),
schema: {query: {start_period: Schema.UintRequired, count: Schema.UintRequired}},
},
resp: {
data: {
toJson: (data, meta) => {
const json: unknown[] = [];
for (const [i, update] of data.entries()) {
json.push(getPostAltairForkTypes(meta.versions[i]).LightClientUpdate.toJson(update));
}
return json;
},
fromJson: (data, meta) => {
const updates = data as unknown[];
const value: LightClientUpdate[] = [];
for (let i = 0; i < updates.length; i++) {
const version = meta.versions[i];
value.push(getPostAltairForkTypes(version).LightClientUpdate.fromJson(updates[i]));
}
return value;
},
serialize: (data, meta) => {
const chunks: Uint8Array[] = [];
for (const [i, update] of data.entries()) {
const version = meta.versions[i];
const config = cachedBeaconConfig();
const epoch = Math.floor(update.attestedHeader.beacon.slot / SLOTS_PER_EPOCH);
const forkDigest = config.forkBoundary2ForkDigest(config.getForkBoundaryAtEpoch(epoch));
const serialized = getPostAltairForkTypes(version).LightClientUpdate.serialize(update);
const length = ssz.UintNum64.serialize(4 + serialized.length);
chunks.push(length, forkDigest, serialized);
}
return Buffer.concat(chunks);
},
deserialize: (data) => {
let offset = 0;
const updates: LightClientUpdate[] = [];
while (offset < data.length) {
const length = ssz.UintNum64.deserialize(data.subarray(offset, offset + 8));
const forkDigest = ssz.ForkDigest.deserialize(data.subarray(offset + 8, offset + 12));
const {fork: version} = cachedBeaconConfig().forkDigest2ForkBoundary(forkDigest);
updates.push(
getPostAltairForkTypes(version).LightClientUpdate.deserialize(
data.subarray(offset + 12, offset + 8 + length)
)
);
offset += 8 + length;
}
return updates;
},
},
meta: {
toJson: (meta) => meta,
fromJson: (val) => val as {versions: ForkName[]},
toHeadersObject: (meta) => ({
[MetaHeader.Version]: meta.versions.join(","),
}),
fromHeaders: (headers) => {
const versions = headers.getOrDefault(MetaHeader.Version, "");
return {versions: versions === "" ? [] : (versions.split(",") as ForkName[])};
},
},
transform: {
toResponse: (data, meta) => {
const updates = data as unknown[];
const resp: unknown[] = [];
for (let i = 0; i < updates.length; i++) {
resp.push({data: updates[i], version: (meta as {versions: string[]}).versions[i]});
}
return resp;
},
fromResponse: (resp) => {
if (!Array.isArray(resp)) {
throw Error("JSON is not an array");
}
const updates: LightClientUpdate[] = [];
const meta: {versions: ForkName[]} = {versions: []};
for (const {data, version} of resp as {data: LightClientUpdate; version: string}[]) {
updates.push(data);
meta.versions.push(toForkName(version));
}
return {data: updates, meta};
},
},
},
},
getLightClientOptimisticUpdate: {
url: "/eth/v1/beacon/light_client/optimistic_update",
method: "GET",
req: EmptyRequestCodec,
resp: {
data: WithVersion((fork) => getPostAltairForkTypes(fork).LightClientOptimisticUpdate),
meta: VersionCodec,
},
},
getLightClientFinalityUpdate: {
url: "/eth/v1/beacon/light_client/finality_update",
method: "GET",
req: EmptyRequestCodec,
resp: {
data: WithVersion((fork) => getPostAltairForkTypes(fork).LightClientFinalityUpdate),
meta: VersionCodec,
},
},
getLightClientBootstrap: {
url: "/eth/v1/beacon/light_client/bootstrap/{block_root}",
method: "GET",
req: {
writeReq: ({blockRoot}) => ({params: {block_root: blockRoot}}),
parseReq: ({params}) => ({blockRoot: params.block_root}),
schema: {params: {block_root: Schema.StringRequired}},
},
resp: {
data: WithVersion((fork) => getPostAltairForkTypes(fork).LightClientBootstrap),
meta: VersionCodec,
},
},
getLightClientCommitteeRoot: {
url: "/eth/v0/beacon/light_client/committee_root",
method: "GET",
req: {
writeReq: ({startPeriod, count}) => ({query: {start_period: startPeriod, count}}),
parseReq: ({query}) => ({startPeriod: query.start_period, count: query.count}),
schema: {query: {start_period: Schema.UintRequired, count: Schema.UintRequired}},
},
resp: {
data: HashListType,
meta: EmptyMetaCodec,
},
},
};
}