UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

441 lines • 24.2 kB
import { routes } from "@lodestar/api"; import { ApiError } from "@lodestar/api/server"; import { ForkName, SLOTS_PER_HISTORICAL_ROOT, isForkPostBellatrix, isForkPostElectra, isForkPostFulu, } from "@lodestar/params"; import { computeEpochAtSlot, computeTimeAtSlot, reconstructFullBlockOrContents, signedBeaconBlockToBlinded, } from "@lodestar/state-transition"; import { ProducedBlockSource, isSignedBlockContents, } from "@lodestar/types"; import { fromHex, sleep, toHex, toRootHex } from "@lodestar/utils"; import { BlobsSource, BlockInputType, BlockSource, getBlockInput, } from "../../../../chain/blocks/types.js"; import { verifyBlocksInEpoch } from "../../../../chain/blocks/verifyBlock.js"; import { BlockError, BlockErrorCode, BlockGossipError } from "../../../../chain/errors/index.js"; import { validateGossipBlock } from "../../../../chain/validation/block.js"; import { OpSource } from "../../../../chain/validatorMonitor.js"; import { NetworkEvent } from "../../../../network/index.js"; import { computeBlobSidecars, kzgCommitmentToVersionedHash } from "../../../../util/blobs.js"; import { isOptimisticBlock } from "../../../../util/forkChoice.js"; import { promiseAllMaybeAsync } from "../../../../util/promises.js"; import { assertUniqueItems } from "../../utils.js"; import { getBlockResponse, toBeaconHeaderResponse } from "./utils.js"; /** * Validator clock may be advanced from beacon's clock. If the validator requests a resource in a * future slot, wait some time instead of rejecting the request because it's in the future */ const MAX_API_CLOCK_DISPARITY_MS = 1000; /** * PeerID of identity keypair to signal self for score reporting */ const IDENTITY_PEER_ID = ""; // TODO: Compute identity keypair export function getBeaconBlockApi({ chain, config, metrics, network, db, }) { const publishBlock = async ({ signedBlockOrContents, broadcastValidation }, _context, opts = {}) => { const seenTimestampSec = Date.now() / 1000; let blockForImport, signedBlock, blobSidecars; if (isSignedBlockContents(signedBlockOrContents)) { ({ signedBlock } = signedBlockOrContents); blobSidecars = computeBlobSidecars(config, signedBlock, signedBlockOrContents); const blockData = { fork: config.getForkName(signedBlock.message.slot), blobs: blobSidecars, blobsSource: BlobsSource.api, }; blockForImport = getBlockInput.availableData(config, signedBlock, BlockSource.api, blockData); } else { signedBlock = signedBlockOrContents; blobSidecars = []; blockForImport = getBlockInput.preData(config, signedBlock, BlockSource.api); } // check what validations have been requested before broadcasting and publishing the block // TODO: add validation time to metrics broadcastValidation = broadcastValidation ?? routes.beacon.BroadcastValidation.gossip; // if block is locally produced, full or blinded, it already is 'consensus' validated as it went through // state transition to produce the stateRoot const slot = signedBlock.message.slot; const fork = config.getForkName(slot); const blockRoot = toRootHex(chain.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(signedBlock.message)); // bodyRoot should be the same to produced block const bodyRoot = toRootHex(chain.config.getForkTypes(slot).BeaconBlockBody.hashTreeRoot(signedBlock.message.body)); const blockLocallyProduced = chain.producedBlockRoot.has(blockRoot) || chain.producedBlindedBlockRoot.has(blockRoot); const valLogMeta = { slot, blockRoot, bodyRoot, broadcastValidation, blockLocallyProduced }; switch (broadcastValidation) { case routes.beacon.BroadcastValidation.gossip: { if (!blockLocallyProduced) { try { await validateGossipBlock(config, chain, signedBlock, fork); } catch (error) { if (error instanceof BlockGossipError && error.type.code === BlockErrorCode.ALREADY_KNOWN) { chain.logger.debug("Ignoring known block during publishing", valLogMeta); // Blocks might already be published by another node as part of a fallback setup or DVT cluster // and can reach our node by gossip before the api. The error can be ignored and should not result in a 500 response. return; } chain.logger.error("Gossip validations failed while publishing the block", valLogMeta, error); chain.persistInvalidSszValue(chain.config.getForkTypes(slot).SignedBeaconBlock, signedBlock, "api_reject_gossip_failure"); throw error; } } chain.logger.debug("Gossip checks validated while publishing the block", valLogMeta); break; } case routes.beacon.BroadcastValidation.consensusAndEquivocation: case routes.beacon.BroadcastValidation.consensus: { // check if this beacon node produced the block else run validations if (!blockLocallyProduced) { const parentBlock = chain.forkChoice.getBlock(signedBlock.message.parentRoot); if (parentBlock === null) { network.events.emit(NetworkEvent.unknownBlockParent, { blockInput: blockForImport, peer: IDENTITY_PEER_ID, }); chain.persistInvalidSszValue(chain.config.getForkTypes(slot).SignedBeaconBlock, signedBlock, "api_reject_parent_unknown"); throw new BlockError(signedBlock, { code: BlockErrorCode.PARENT_UNKNOWN, parentRoot: toRootHex(signedBlock.message.parentRoot), }); } try { await verifyBlocksInEpoch.call(chain, parentBlock, [blockForImport], { ...opts, verifyOnly: true, skipVerifyBlockSignatures: true, skipVerifyExecutionPayload: true, seenTimestampSec, }); } catch (error) { chain.logger.error("Consensus checks failed while publishing the block", valLogMeta, error); chain.persistInvalidSszValue(chain.config.getForkTypes(slot).SignedBeaconBlock, signedBlock, "api_reject_consensus_failure"); throw error; } } chain.logger.debug("Consensus validated while publishing block", valLogMeta); if (broadcastValidation === routes.beacon.BroadcastValidation.consensusAndEquivocation) { const message = `Equivocation checks not yet implemented for broadcastValidation=${broadcastValidation}`; if (chain.opts.broadcastValidationStrictness === "error") { throw Error(message); } chain.logger.warn(message, valLogMeta); } break; } case routes.beacon.BroadcastValidation.none: { chain.logger.debug("Skipping broadcast validation", valLogMeta); break; } default: { // error or log warning we do not support this validation const message = `Broadcast validation of ${broadcastValidation} type not implemented yet`; if (chain.opts.broadcastValidationStrictness === "error") { throw Error(message); } chain.logger.warn(message, valLogMeta); } } // Simple implementation of a pending block queue. Keeping the block here recycles the API logic, and keeps the // REST request promise without any extra infrastructure. const msToBlockSlot = computeTimeAtSlot(config, blockForImport.block.message.slot, chain.genesisTime) * 1000 - Date.now(); if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) { // If block is a bit early, hold it in a promise. Equivalent to a pending queue. await sleep(msToBlockSlot); } // TODO: Validate block const delaySec = seenTimestampSec - (chain.genesisTime + blockForImport.block.message.slot * config.SECONDS_PER_SLOT); metrics?.gossipBlock.elapsedTimeTillReceived.observe({ source: OpSource.api }, delaySec); chain.validatorMonitor?.registerBeaconBlock(OpSource.api, delaySec, blockForImport.block.message); chain.logger.info("Publishing block", valLogMeta); const publishPromises = [ // Send the block, regardless of whether or not it is valid. The API // specification is very clear that this is the desired behaviour. // // i) Publish blobs and block before importing so that network can see them asap // ii) publish block first because // a) as soon as node sees block they can start processing it while blobs arrive // b) getting block first allows nodes to use getBlobs from local ELs and save // import latency and hopefully bandwidth () => network.publishBeaconBlock(signedBlock), ...blobSidecars.map((blobSidecar) => () => network.publishBlobSidecar(blobSidecar)), () => // there is no rush to persist block since we published it to gossip anyway chain .processBlock(blockForImport, { ...opts, eagerPersistBlock: false }) .catch((e) => { if (e instanceof BlockError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) { network.events.emit(NetworkEvent.unknownBlockParent, { blockInput: blockForImport, peer: IDENTITY_PEER_ID, }); } throw e; }), ]; await promiseAllMaybeAsync(publishPromises); if (chain.emitter.listenerCount(routes.events.EventType.blockGossip)) { chain.emitter.emit(routes.events.EventType.blockGossip, { slot, block: blockRoot }); } if (chain.emitter.listenerCount(routes.events.EventType.blobSidecar) && blockForImport.type === BlockInputType.availableData && (blockForImport.blockData.fork === ForkName.deneb || blockForImport.blockData.fork === ForkName.electra)) { const { blobs } = blockForImport.blockData; for (const blobSidecar of blobs) { const { index, kzgCommitment } = blobSidecar; chain.emitter.emit(routes.events.EventType.blobSidecar, { blockRoot, slot, index, kzgCommitment: toHex(kzgCommitment), versionedHash: toHex(kzgCommitmentToVersionedHash(kzgCommitment)), }); } } }; const publishBlindedBlock = async ({ signedBlindedBlock }, context, opts = {}) => { const slot = signedBlindedBlock.message.slot; const blockRoot = toRootHex(chain.config .getPostBellatrixForkTypes(signedBlindedBlock.message.slot) .BlindedBeaconBlock.hashTreeRoot(signedBlindedBlock.message)); const fork = config.getForkName(slot); // Either the payload/blobs are cached from i) engine locally or ii) they are from the builder // // executionPayload can be null or a real payload in locally produced so check for presence of root const executionPayload = chain.producedBlockRoot.get(blockRoot); if (executionPayload !== undefined) { const source = ProducedBlockSource.engine; chain.logger.debug("Reconstructing signedBlockOrContents", { slot, blockRoot, source }); const contents = executionPayload ? (chain.producedContentsCache.get(toRootHex(executionPayload.blockHash)) ?? null) : null; const signedBlockOrContents = reconstructFullBlockOrContents(signedBlindedBlock, { executionPayload, contents }); chain.logger.info("Publishing assembled block", { slot, blockRoot, source }); return publishBlock({ signedBlockOrContents }, { ...context, sszBytes: null }, opts); } const source = ProducedBlockSource.builder; if (isForkPostFulu(fork)) { await submitBlindedBlockToBuilder(chain, { data: signedBlindedBlock, bytes: context?.sszBytes, }); chain.logger.info("Submitted blinded block to builder for publishing", { slot, blockRoot }); } else { // TODO: After fulu is live and all builders support submitBlindedBlockV2, we can safely remove // this code block and related functions chain.logger.debug("Reconstructing signedBlockOrContents", { slot, blockRoot, source }); const signedBlockOrContents = await reconstructBuilderBlockOrContents(chain, { data: signedBlindedBlock, bytes: context?.sszBytes, }); // the full block is published by relay and it's possible that the block is already known to us // by gossip // // see: https://github.com/ChainSafe/lodestar/issues/5404 chain.logger.info("Publishing assembled block", { slot, blockRoot, source }); return publishBlock({ signedBlockOrContents }, { ...context, sszBytes: null }, { ...opts, ignoreIfKnown: true }); } }; return { async getBlockHeaders({ slot, parentRoot }) { // TODO - SLOW CODE: This code seems like it could be improved // If one block in the response contains an optimistic block, mark the entire response as optimistic let executionOptimistic = false; // If one block in the response is non finalized, mark the entire response as unfinalized let finalized = true; const result = []; if (parentRoot) { const finalizedBlock = await db.blockArchive.getByParentRoot(fromHex(parentRoot)); if (finalizedBlock) { result.push(toBeaconHeaderResponse(config, finalizedBlock, true)); } const nonFinalizedBlocks = chain.forkChoice.getBlockSummariesByParentRoot(parentRoot); await Promise.all(nonFinalizedBlocks.map(async (summary) => { const block = await db.block.get(fromHex(summary.blockRoot)); if (block) { const canonical = chain.forkChoice.getCanonicalBlockAtSlot(block.message.slot); if (canonical) { result.push(toBeaconHeaderResponse(config, block, canonical.blockRoot === summary.blockRoot)); if (isOptimisticBlock(canonical)) { executionOptimistic = true; } // Block from hot db which only contains unfinalized blocks finalized = false; } } })); return { data: result.filter((item) => // skip if no slot filter !(slot !== undefined && slot !== 0) || item.header.message.slot === slot), meta: { executionOptimistic, finalized }, }; } const headSlot = chain.forkChoice.getHead().slot; if (!parentRoot && slot === undefined) { slot = headSlot; } if (slot !== undefined) { // future slot if (slot > headSlot) { return { data: [], meta: { executionOptimistic: false, finalized: false } }; } const canonicalBlock = await chain.getCanonicalBlockAtSlot(slot); // skip slot if (!canonicalBlock) { return { data: [], meta: { executionOptimistic: false, finalized: false } }; } const canonicalRoot = config .getForkTypes(canonicalBlock.block.message.slot) .BeaconBlock.hashTreeRoot(canonicalBlock.block.message); result.push(toBeaconHeaderResponse(config, canonicalBlock.block, true)); if (!canonicalBlock.finalized) { finalized = false; } // fork blocks // TODO: What is this logic? await Promise.all(chain.forkChoice.getBlockSummariesAtSlot(slot).map(async (summary) => { if (isOptimisticBlock(summary)) { executionOptimistic = true; } finalized = false; if (summary.blockRoot !== toRootHex(canonicalRoot)) { const block = await db.block.get(fromHex(summary.blockRoot)); if (block) { result.push(toBeaconHeaderResponse(config, block)); } } })); } return { data: result, meta: { executionOptimistic, finalized }, }; }, async getBlockHeader({ blockId }) { const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); return { data: toBeaconHeaderResponse(config, block, true), meta: { executionOptimistic, finalized }, }; }, async getBlockV2({ blockId }) { const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); return { data: block, meta: { executionOptimistic, finalized, version: config.getForkName(block.message.slot), }, }; }, async getBlindedBlock({ blockId }) { const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); const fork = config.getForkName(block.message.slot); return { data: isForkPostBellatrix(fork) ? signedBeaconBlockToBlinded(config, block) : block, meta: { executionOptimistic, finalized, version: fork, }, }; }, async getBlockAttestations({ blockId }) { const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); const fork = config.getForkName(block.message.slot); if (isForkPostElectra(fork)) { throw new ApiError(400, `Use getBlockAttestationsV2 to retrieve block attestations for post-electra fork=${fork}`); } return { data: block.message.body.attestations, meta: { executionOptimistic, finalized }, }; }, async getBlockAttestationsV2({ blockId }) { const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); return { data: block.message.body.attestations, meta: { executionOptimistic, finalized, version: config.getForkName(block.message.slot) }, }; }, async getBlockRoot({ blockId }) { // Fast path: From head state already available in memory get historical blockRoot const slot = typeof blockId === "string" ? parseInt(blockId) : blockId; if (!Number.isNaN(slot)) { const head = chain.forkChoice.getHead(); if (slot === head.slot) { return { data: { root: fromHex(head.blockRoot) }, meta: { executionOptimistic: isOptimisticBlock(head), finalized: false }, }; } if (slot < head.slot && head.slot <= slot + SLOTS_PER_HISTORICAL_ROOT) { const state = chain.getHeadState(); return { data: { root: state.blockRoots.get(slot % SLOTS_PER_HISTORICAL_ROOT) }, meta: { executionOptimistic: isOptimisticBlock(head), finalized: computeEpochAtSlot(slot) <= chain.forkChoice.getFinalizedCheckpoint().epoch, }, }; } } else if (blockId === "head") { const head = chain.forkChoice.getHead(); return { data: { root: fromHex(head.blockRoot) }, meta: { executionOptimistic: isOptimisticBlock(head), finalized: false }, }; } // Slow path const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); return { data: { root: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message) }, meta: { executionOptimistic, finalized }, }; }, publishBlock, publishBlindedBlock, async publishBlindedBlockV2(args, context, opts) { await publishBlindedBlock(args, context, opts); }, async publishBlockV2(args, context, opts) { await publishBlock(args, context, opts); }, async getBlobSidecars({ blockId, indices }) { assertUniqueItems(indices, "Duplicate indices provided"); const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); const blockRoot = config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message); let { blobSidecars } = (await db.blobSidecars.get(blockRoot)) ?? {}; if (!blobSidecars) { ({ blobSidecars } = (await db.blobSidecarsArchive.get(block.message.slot)) ?? {}); } if (!blobSidecars) { throw Error(`blobSidecars not found in db for slot=${block.message.slot} root=${toRootHex(blockRoot)}`); } return { data: indices ? blobSidecars.filter(({ index }) => indices.includes(index)) : blobSidecars, meta: { executionOptimistic, finalized, version: config.getForkName(block.message.slot), }, }; }, }; } async function reconstructBuilderBlockOrContents(chain, signedBlindedBlock) { const executionBuilder = chain.executionBuilder; if (!executionBuilder) { throw Error("executionBuilder required to publish SignedBlindedBeaconBlock"); } const signedBlockOrContents = await executionBuilder.submitBlindedBlock(signedBlindedBlock); return signedBlockOrContents; } async function submitBlindedBlockToBuilder(chain, signedBlindedBlock) { const executionBuilder = chain.executionBuilder; if (!executionBuilder) { throw Error("executionBuilder required to submit SignedBlindedBeaconBlock to builder"); } await executionBuilder.submitBlindedBlockNoResponse(signedBlindedBlock); } //# sourceMappingURL=index.js.map