@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
297 lines • 12.9 kB
JavaScript
import { Repository } from "@lodestar/db";
import { ForkSeq, SLOTS_PER_EPOCH } from "@lodestar/params";
import { computeEpochAtSlot, computeStartSlotAtEpoch, getIndexedAttestation, isStatePostCapella, } from "@lodestar/state-transition";
import { ssz } from "@lodestar/types";
import { fromHex, toHex, toRootHex } from "@lodestar/utils";
import { getStateSlotFromBytes } from "../../../util/multifork.js";
import { ProfileThread, profileThread, writeHeapSnapshot } from "../../../util/profile.js";
import { getStateResponseWithRegen } from "../beacon/state/utils.js";
import { ApiError } from "../errors.js";
import { getAttesterSlashingsFromIndexedAttestations } from "./attesterSlashing.js";
export function getLodestarApi({ chain, config, db, network, sync, }) {
let writingHeapdump = false;
let writingProfile = false;
// for NodeJS, profile the whole epoch
// for Bun, profile 1 slot. Otherwise it will either crash the app, and/or inspector cannot render the profile
const defaultProfileMs = globalThis.Bun ? config.SLOT_DURATION_MS : SLOTS_PER_EPOCH * config.SLOT_DURATION_MS;
return {
async writeHeapdump({ thread = "main", dirpath = "." }) {
if (writingHeapdump) {
throw Error("Already writing heapdump");
}
try {
writingHeapdump = true;
let filepath;
switch (thread) {
case "network":
filepath = await network.writeNetworkHeapSnapshot("network_thread", dirpath);
break;
case "discv5":
filepath = await network.writeDiscv5HeapSnapshot("discv5_thread", dirpath);
break;
default:
// main thread
filepath = await writeHeapSnapshot("main_thread", dirpath);
break;
}
return { data: { filepath } };
}
finally {
writingHeapdump = false;
}
},
async writeProfile({ thread = "network", duration = defaultProfileMs, dirpath = "." }) {
if (writingProfile) {
throw Error("Already writing network profile");
}
writingProfile = true;
try {
let filepath;
switch (thread) {
case "network":
filepath = await network.writeNetworkThreadProfile(duration, dirpath);
break;
case "discv5":
filepath = await network.writeDiscv5Profile(duration, dirpath);
break;
default:
// main thread
filepath = await profileThread(ProfileThread.MAIN, duration, dirpath);
break;
}
return { data: { result: filepath } };
}
finally {
writingProfile = false;
}
},
async getLatestWeakSubjectivityCheckpointEpoch() {
const state = chain.getHeadState();
return { data: state.getLatestWeakSubjectivityCheckpointEpoch() };
},
async getSyncChainsDebugState() {
return { data: sync.getSyncChainsDebugState() };
},
async getGossipQueueItems({ gossipType }) {
return {
data: await network.dumpGossipQueue(gossipType),
};
},
async getRegenQueueItems() {
return {
data: chain.regen.jobQueue.getItems().map((item) => ({
key: item.args[0].key,
args: regenRequestToJson(config, item.args[0]),
addedTimeMs: item.addedTimeMs,
})),
};
},
async getBlockProcessorQueueItems() {
return {
// biome-ignore lint/complexity/useLiteralKeys: The `blockProcessor` is a protected attribute
data: chain["blockProcessor"].jobQueue.getItems().map((item) => {
const [blockInputs, _payloadEnvelopes, opts] = item.args;
return {
blockSlots: blockInputs.map((blockInput) => blockInput.slot),
jobOpts: opts,
addedTimeMs: item.addedTimeMs,
};
}),
};
},
async getStateCacheItems() {
return { data: chain.regen.dumpCacheSummary() };
},
async getGossipPeerScoreStats() {
return {
data: Object.entries(await network.dumpGossipPeerScoreStats()).map(([peerId, stats]) => ({ peerId, ...stats })),
};
},
async getLodestarPeerScoreStats() {
return { data: await network.dumpPeerScoreStats() };
},
async runGC() {
if (!global.gc)
throw Error("You must expose GC running the Node.js process with 'node --expose_gc'");
global.gc();
},
async dropStateCache() {
chain.regen.dropCache();
},
async connectPeer({ peerId, multiaddrs }) {
await network.connectToPeer(peerId, multiaddrs);
},
async disconnectPeer({ peerId }) {
await network.disconnectPeer(peerId);
},
async addDirectPeer({ peer }) {
const peerId = await network.addDirectPeer(peer);
if (peerId === null) {
throw new ApiError(400, `Failed to add direct peer: invalid peer address or ENR "${peer}"`);
}
return { data: { peerId } };
},
async removeDirectPeer({ peerId }) {
const removed = await network.removeDirectPeer(peerId);
return { data: { removed } };
},
async getDirectPeers() {
return { data: await network.getDirectPeers() };
},
async getPeers({ state, direction }) {
const peers = (await network.dumpPeers()).filter((nodePeer) => (!state || state.length === 0 || state.includes(nodePeer.state)) &&
(!direction || direction.length === 0 || (nodePeer.direction && direction.includes(nodePeer.direction))));
return {
data: peers,
meta: { count: peers.length },
};
},
async getBlacklistedBlocks() {
return {
data: Array.from(chain.blacklistedBlocks.entries()).map(([root, slot]) => ({ root, slot })),
};
},
async discv5GetKadValues() {
return {
data: await network.dumpDiscv5KadValues(),
};
},
async dumpDbBucketKeys({ bucket }) {
for (const repo of Object.values(db)) {
// biome-ignore lint/complexity/useLiteralKeys: `bucket` is protected and `bucketId` is private
if (repo instanceof Repository && (String(repo["bucket"]) === bucket || repo["bucketId"] === bucket)) {
return { data: stringifyKeys(await repo.keys()) };
}
}
throw Error(`Unknown Bucket '${bucket}'`);
},
async dumpDbStateIndex() {
return { data: await db.stateArchive.dumpRootIndexEntries() };
},
async getHistoricalSummaries({ stateId }) {
const { state, executionOptimistic, finalized } = await getStateResponseWithRegen(chain, stateId);
const stateView = state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state;
const fork = config.getForkName(stateView.slot);
if (ForkSeq[fork] < ForkSeq.capella) {
throw new Error("Historical summaries are not supported before Capella");
}
if (!isStatePostCapella(stateView)) {
throw new Error("Expected Capella state for historical summaries");
}
const { gindex } = ssz[fork].BeaconState.getPathInfo(["historicalSummaries"]);
const proof = stateView.getSingleProof(gindex);
return {
data: {
slot: stateView.slot,
historicalSummaries: stateView.historicalSummaries,
proof: proof,
},
meta: { executionOptimistic, finalized, version: fork },
};
},
// the optional checkpoint is in root:epoch format
async getPersistedCheckpointState({ checkpointId }) {
const checkpoint = checkpointId ? getCheckpointFromArg(checkpointId) : undefined;
const stateBytes = await chain.getPersistedCheckpointState(checkpoint);
if (stateBytes === null) {
throw new ApiError(404, checkpointId ? `Checkpoint state not found for id ${checkpointId}` : "Latest safe checkpoint state not found");
}
const slot = getStateSlotFromBytes(stateBytes);
return {
data: stateBytes,
meta: {
version: config.getForkName(slot),
},
};
},
async getMonitoredValidatorIndices() {
return {
data: chain.validatorMonitor?.getMonitoredValidatorIndices() ?? [],
};
},
async getCustodyInfo() {
const { custodyColumns, targetCustodyGroupCount } = chain.custodyConfig;
return {
data: {
earliestCustodiedSlot: chain.earliestAvailableSlot,
custodyGroupCount: targetCustodyGroupCount,
custodyColumns,
},
};
},
async getAttesterSlashingsFromBlocks({ signedBlocks }) {
const attestations = new Map();
for (const block of signedBlocks) {
const attestationsOfABlock = block.message.body.attestations;
for (const attestation of attestationsOfABlock) {
const epoch = computeEpochAtSlot(attestation.data.slot);
let attestationsPerEpoch = attestations.get(epoch);
if (!attestationsPerEpoch) {
attestationsPerEpoch = [];
attestations.set(epoch, attestationsPerEpoch);
}
attestationsPerEpoch.push(attestation);
}
}
const indexedAttestations = [];
// Assume all blocks are from the same fork
const forkSeq = config.getForkSeq(signedBlocks[0].message.slot);
for (const [epoch, attestationsPerEpoch] of attestations) {
const slot = computeStartSlotAtEpoch(epoch);
const { state } = await getStateResponseWithRegen(chain, slot);
const stateView = state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state;
const shuffling = stateView.getShufflingAtEpoch(epoch);
for (const attestation of attestationsPerEpoch) {
indexedAttestations.push(getIndexedAttestation(shuffling, forkSeq, attestation));
}
}
const result = getAttesterSlashingsFromIndexedAttestations(forkSeq, indexedAttestations);
return {
data: result,
meta: {
version: config.getForkName(signedBlocks[0].message.slot),
},
};
},
};
}
function regenRequestToJson(config, regenRequest) {
switch (regenRequest.key) {
case "getBlockSlotState":
return {
root: regenRequest.args[0],
slot: regenRequest.args[1],
};
case "getPreState": {
const slot = regenRequest.args[0].slot;
return {
root: toRootHex(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(regenRequest.args[0])),
slot,
};
}
case "getState":
return {
root: regenRequest.args[0],
};
}
}
const CHECKPOINT_REGEX = /^(?:0x)?([0-9a-f]{64}):([0-9]+)$/;
/**
* Extract a checkpoint from a string in the format `rootHex:epoch`.
*/
export function getCheckpointFromArg(checkpointStr) {
const match = CHECKPOINT_REGEX.exec(checkpointStr.toLowerCase());
if (!match) {
throw new ApiError(400, `Could not parse checkpoint string: ${checkpointStr}`);
}
return { root: fromHex(match[1]), epoch: parseInt(match[2]) };
}
function stringifyKeys(keys) {
return keys.map((key) => {
if (key instanceof Uint8Array) {
return toHex(key);
}
return `${key}`;
});
}
//# sourceMappingURL=index.js.map