@lodestar/api
Version:
A Typescript REST client for the Ethereum Consensus API
605 lines (575 loc) • 15.9 kB
text/typescript
import {ContainerType, Type, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {ArrayOf, BeaconState, Epoch, RootHex, Slot, ValidatorIndex, ssz} from "@lodestar/types";
import {
EmptyArgs,
EmptyMeta,
EmptyRequest,
EmptyRequestCodec,
EmptyResponseCodec,
EmptyResponseData,
JsonOnlyResponseCodec,
WithVersion,
} from "../../utils/codecs.js";
import {Endpoint, RouteDefinitions, Schema} from "../../utils/index.js";
import {
ExecutionOptimisticFinalizedAndVersionCodec,
ExecutionOptimisticFinalizedAndVersionMeta,
VersionCodec,
VersionMeta,
} from "../../utils/metadata.js";
import {StateArgs} from "./beacon/state.js";
import {FilterGetPeers, NodePeer, PeerDirection, PeerState} from "./node.js";
export type SyncChainDebugState = {
targetRoot: string | null;
targetSlot: number | null;
syncType: string;
status: string;
startEpoch: number;
peers: number;
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
batches: any[];
};
export type GossipQueueItem = {
topic: unknown;
propagationSource: string;
data: Uint8Array;
addedTimeMs: number;
seenTimestampSec: number;
};
export type PeerScoreStat = {
peerId: string;
lodestarScore: number;
gossipScore: number;
ignoreNegativeGossipScore: boolean;
score: number;
lastUpdate: number;
};
export type GossipPeerScoreStat = {
peerId: string;
// + Other un-typed options
};
/**
* A multiaddr with peer ID or ENR string.
*
* Supported formats:
* - Multiaddr with peer ID: `/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7...`
* - ENR: `enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOo...`
*
* For multiaddrs, the string must contain a /p2p/ component with the peer ID.
* For ENRs, the TCP multiaddr and peer ID are extracted from the encoded record.
*/
export type DirectPeer = string;
export type RegenQueueItem = {
key: string;
args: unknown;
addedTimeMs: number;
};
export type BlockProcessorQueueItem = {
blockSlots: Slot[];
jobOpts: Record<string, string | number | boolean | undefined>;
addedTimeMs: number;
};
export type StateCacheItem = {
slot: Slot;
root: RootHex;
/** Total number of reads */
reads: number;
/** Unix timestamp (ms) of the last read */
lastRead: number;
checkpointState: boolean;
};
export type LodestarNodePeer = NodePeer & {
agentVersion: string;
status: unknown | null;
metadata: unknown | null;
agentClient: string;
lastReceivedMsgUnixTsMs: number;
lastStatusUnixTsMs: number;
connectedUnixTsMs: number;
};
export type BlacklistedBlock = {root: RootHex; slot: Slot | null};
export type LodestarThreadType = "main" | "network" | "discv5";
const HistoricalSummariesResponseType = new ContainerType(
{
slot: ssz.Slot,
historicalSummaries: ssz.capella.HistoricalSummaries,
proof: ArrayOf(ssz.Bytes8),
},
{jsonCase: "eth2"}
);
export type HistoricalSummariesResponse = ValueOf<typeof HistoricalSummariesResponseType>;
export type CustodyInfo = {
/** Earliest slot for which the node has custodied data columns */
earliestCustodiedSlot: Slot;
/** Number of custody groups the node is responsible for */
custodyGroupCount: number;
/** List of column indices the node is custodying */
custodyColumns: number[];
};
export type Endpoints = {
/** Trigger to write a heapdump to disk at `dirpath`. May take > 1min */
writeHeapdump: Endpoint<
"POST",
{thread?: LodestarThreadType; dirpath?: string},
{query: {thread?: LodestarThreadType; dirpath?: string}},
{filepath: string},
EmptyMeta
>;
/** Trigger to write 10m network thread profile to disk */
writeProfile: Endpoint<
"POST",
{
thread?: LodestarThreadType;
duration?: number;
dirpath?: string;
},
{query: {thread?: LodestarThreadType; duration?: number; dirpath?: string}},
{result: string},
EmptyMeta
>;
/** TODO: description */
getLatestWeakSubjectivityCheckpointEpoch: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
Epoch,
EmptyMeta
>;
/** TODO: description */
getSyncChainsDebugState: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
SyncChainDebugState[],
EmptyMeta
>;
/** Dump all items in a gossip queue, by gossipType */
getGossipQueueItems: Endpoint<
// ⏎
"GET",
{gossipType: string},
{params: {gossipType: string}},
unknown[],
EmptyMeta
>;
/** Dump all items in the regen queue */
getRegenQueueItems: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
RegenQueueItem[],
EmptyMeta
>;
/** Dump all items in the block processor queue */
getBlockProcessorQueueItems: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
BlockProcessorQueueItem[],
EmptyMeta
>;
/** Dump a summary of the states in the block state cache and checkpoint state cache */
getStateCacheItems: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
StateCacheItem[],
EmptyMeta
>;
/** Dump peer gossip stats by peer */
getGossipPeerScoreStats: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
GossipPeerScoreStat[],
EmptyMeta
>;
/** Dump lodestar score stats by peer */
getLodestarPeerScoreStats: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
PeerScoreStat[],
EmptyMeta
>;
/** Run GC with `global.gc()` */
runGC: Endpoint<
// ⏎
"POST",
EmptyArgs,
EmptyRequest,
EmptyResponseData,
EmptyMeta
>;
/** Drop all states in the state cache */
dropStateCache: Endpoint<
// ⏎
"POST",
EmptyArgs,
EmptyRequest,
EmptyResponseData,
EmptyMeta
>;
/** Connect to peer at this multiaddress */
connectPeer: Endpoint<
// ⏎
"POST",
{peerId: string; multiaddrs: string[]},
{query: {peerId: string; multiaddr: string[]}},
EmptyResponseData,
EmptyMeta
>;
/** Disconnect peer */
disconnectPeer: Endpoint<
// ⏎
"POST",
{peerId: string},
{query: {peerId: string}},
EmptyResponseData,
EmptyMeta
>;
/**
* Add a direct peer at runtime.
* Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation.
* Accepts either a multiaddr with peer ID or an ENR string.
*/
addDirectPeer: Endpoint<
// ⏎
"POST",
{peer: DirectPeer},
{query: {peer: string}},
{peerId: string},
EmptyMeta
>;
/** Remove a peer from direct peers */
removeDirectPeer: Endpoint<
// ⏎
"DELETE",
{peerId: string},
{query: {peerId: string}},
{removed: boolean},
EmptyMeta
>;
/** Get list of direct peer IDs */
getDirectPeers: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
string[],
EmptyMeta
>;
/** Same to node api with new fields */
getPeers: Endpoint<
"GET",
FilterGetPeers,
{query: {state?: PeerState[]; direction?: PeerDirection[]}},
LodestarNodePeer[],
{count: number}
>;
/** Returns root/slot of blacklisted blocks */
getBlacklistedBlocks: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
BlacklistedBlock[],
EmptyMeta
>;
/** Returns historical summaries and proof for a given state ID */
getHistoricalSummaries: Endpoint<
// ⏎
"GET",
StateArgs,
{params: {state_id: string}},
HistoricalSummariesResponse,
ExecutionOptimisticFinalizedAndVersionMeta
>;
getPersistedCheckpointState: Endpoint<
"GET",
{
/** The checkpoint in `<root>:<epoch>` format to be returned instead of the latest safe checkpoint state */
checkpointId?: string;
},
{query: {checkpoint_id?: string}},
BeaconState,
VersionMeta
>;
/**
* Returns the validator indices that are currently being monitored by the validator monitor.
*/
getMonitoredValidatorIndices: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
ValidatorIndex[],
EmptyMeta
>;
/** Dump Discv5 Kad values */
discv5GetKadValues: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
string[],
EmptyMeta
>;
/**
* Dump level-db entry keys for a given Bucket declared in code, or for all buckets.
*/
dumpDbBucketKeys: Endpoint<
"GET",
{
/** Must be the string name of a bucket entry: `allForks_blockArchive` */
bucket: string;
},
{params: {bucket: string}},
string[],
EmptyMeta
>;
/** Return all entries in the StateArchive index with bucket index_stateArchiveRootIndex */
dumpDbStateIndex: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
{root: RootHex; slot: Slot}[],
EmptyMeta
>;
/** Get custody information for data columns */
getCustodyInfo: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
CustodyInfo,
EmptyMeta
>;
};
export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpoints> {
return {
writeHeapdump: {
url: "/eth/v1/lodestar/write_heapdump",
method: "POST",
req: {
writeReq: ({thread, dirpath}) => ({query: {thread, dirpath}}),
parseReq: ({query}) => ({thread: query.thread, dirpath: query.dirpath}),
schema: {query: {thread: Schema.String, dirpath: Schema.String}},
},
resp: JsonOnlyResponseCodec,
},
writeProfile: {
url: "/eth/v1/lodestar/write_profile",
method: "POST",
req: {
writeReq: ({thread, duration, dirpath}) => ({query: {thread, duration, dirpath}}),
parseReq: ({query}) => ({thread: query.thread, duration: query.duration, dirpath: query.dirpath}),
schema: {query: {thread: Schema.String, duration: Schema.Uint, dirpath: Schema.String}},
},
resp: JsonOnlyResponseCodec,
},
getLatestWeakSubjectivityCheckpointEpoch: {
url: "/eth/v1/lodestar/ws_epoch",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getSyncChainsDebugState: {
url: "/eth/v1/lodestar/sync_chains_debug_state",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getGossipQueueItems: {
url: "/eth/v1/lodestar/gossip_queue_items/:gossipType",
method: "GET",
req: {
writeReq: ({gossipType}) => ({params: {gossipType}}),
parseReq: ({params}) => ({gossipType: params.gossipType}),
schema: {params: {gossipType: Schema.StringRequired}},
},
resp: JsonOnlyResponseCodec,
},
getRegenQueueItems: {
url: "/eth/v1/lodestar/regen_queue_items",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getBlockProcessorQueueItems: {
url: "/eth/v1/lodestar/block_processor_queue_items",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getStateCacheItems: {
url: "/eth/v1/lodestar/state_cache_items",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getGossipPeerScoreStats: {
url: "/eth/v1/lodestar/gossip_peer_score_stats",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getLodestarPeerScoreStats: {
url: "/eth/v1/lodestar/lodestar_peer_score_stats",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
runGC: {
url: "/eth/v1/lodestar/gc",
method: "POST",
req: EmptyRequestCodec,
resp: EmptyResponseCodec,
},
dropStateCache: {
url: "/eth/v1/lodestar/drop_state_cache",
method: "POST",
req: EmptyRequestCodec,
resp: EmptyResponseCodec,
},
connectPeer: {
url: "/eth/v1/lodestar/connect_peer",
method: "POST",
req: {
writeReq: ({peerId, multiaddrs}) => ({query: {peerId, multiaddr: multiaddrs}}),
parseReq: ({query}) => ({peerId: query.peerId, multiaddrs: query.multiaddr}),
schema: {query: {peerId: Schema.StringRequired, multiaddr: Schema.StringArray}},
},
resp: EmptyResponseCodec,
},
disconnectPeer: {
url: "/eth/v1/lodestar/disconnect_peer",
method: "POST",
req: {
writeReq: ({peerId}) => ({query: {peerId}}),
parseReq: ({query}) => ({peerId: query.peerId}),
schema: {query: {peerId: Schema.StringRequired}},
},
resp: EmptyResponseCodec,
},
addDirectPeer: {
url: "/eth/v1/lodestar/direct_peers",
method: "POST",
req: {
writeReq: ({peer}) => ({query: {peer}}),
parseReq: ({query}) => ({peer: query.peer}),
schema: {query: {peer: Schema.StringRequired}},
},
resp: JsonOnlyResponseCodec,
},
removeDirectPeer: {
url: "/eth/v1/lodestar/direct_peers",
method: "DELETE",
req: {
writeReq: ({peerId}) => ({query: {peerId}}),
parseReq: ({query}) => ({peerId: query.peerId}),
schema: {query: {peerId: Schema.StringRequired}},
},
resp: JsonOnlyResponseCodec,
},
getDirectPeers: {
url: "/eth/v1/lodestar/direct_peers",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getPeers: {
url: "/eth/v1/lodestar/peers",
method: "GET",
req: {
writeReq: ({state, direction}) => ({query: {state, direction}}),
parseReq: ({query}) => ({state: query.state, direction: query.direction}),
schema: {query: {state: Schema.StringArray, direction: Schema.StringArray}},
},
resp: JsonOnlyResponseCodec,
},
getBlacklistedBlocks: {
url: "/eth/v1/lodestar/blacklisted_blocks",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getHistoricalSummaries: {
url: "/eth/v1/lodestar/states/{state_id}/historical_summaries",
method: "GET",
req: {
writeReq: ({stateId}) => ({params: {state_id: stateId.toString()}}),
parseReq: ({params}) => ({stateId: params.state_id}),
schema: {
params: {state_id: Schema.StringRequired},
},
},
resp: {
data: HistoricalSummariesResponseType,
meta: ExecutionOptimisticFinalizedAndVersionCodec,
},
},
getPersistedCheckpointState: {
url: "/eth/v1/lodestar/persisted_checkpoint_state",
method: "GET",
req: {
writeReq: ({checkpointId}) => ({query: {checkpoint_id: checkpointId}}),
parseReq: ({query}) => ({checkpointId: query.checkpoint_id}),
schema: {
query: {checkpoint_id: Schema.String},
},
},
resp: {
data: WithVersion((fork) => ssz[fork].BeaconState as Type<BeaconState>),
meta: VersionCodec,
},
init: {
// Default timeout is not sufficient to download state
timeoutMs: 5 * 60 * 1000,
},
},
getMonitoredValidatorIndices: {
url: "/eth/v1/lodestar/monitored_validators",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
discv5GetKadValues: {
url: "/eth/v1/debug/discv5_kad_values",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
dumpDbBucketKeys: {
url: "/eth/v1/debug/dump_db_bucket_keys/:bucket",
method: "GET",
req: {
writeReq: ({bucket}) => ({params: {bucket}}),
parseReq: ({params}) => ({bucket: params.bucket}),
schema: {params: {bucket: Schema.String}},
},
resp: JsonOnlyResponseCodec,
},
dumpDbStateIndex: {
url: "/eth/v1/debug/dump_db_state_index",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getCustodyInfo: {
url: "/eth/v1/lodestar/custody_info",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
};
}