UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

297 lines • 12.9 kB
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