UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

824 lines • 46.2 kB
import { routes } from "@lodestar/api"; import { ApiError } from "@lodestar/api/server"; import { PayloadStatus } from "@lodestar/fork-choice"; import { BUILDER_INDEX_SELF_BUILD, NUMBER_OF_COLUMNS, SLOTS_PER_HISTORICAL_ROOT, isForkPostBellatrix, isForkPostDeneb, isForkPostElectra, isForkPostFulu, isForkPostGloas, } from "@lodestar/params"; import { computeEpochAtSlot, computeTimeAtSlot, reconstructSignedBlockContents, signedBeaconBlockToBlinded, signedBlockToSignedHeader, } from "@lodestar/state-transition"; import { ProducedBlockSource, isDenebBlockContents, sszTypesFor, } from "@lodestar/types"; import { fromHex, sleep, toHex, toRootHex } from "@lodestar/utils"; import { BlockInputSource, isBlockInputBlobs, isBlockInputColumns } from "../../../../chain/blocks/blockInput/index.js"; import { PayloadEnvelopeInputSource } from "../../../../chain/blocks/payloadEnvelopeInput/index.js"; import { verifyBlocksInEpoch } from "../../../../chain/blocks/verifyBlock.js"; import { ChainEvent } from "../../../../chain/emitter.js"; import { BlockError, BlockErrorCode, BlockGossipError } from "../../../../chain/errors/index.js"; import { BlockType, } from "../../../../chain/produceBlock/index.js"; import { validateGossipBlock } from "../../../../chain/validation/block.js"; import { validateApiExecutionPayloadEnvelope } from "../../../../chain/validation/executionPayloadEnvelope.js"; import { OpSource } from "../../../../chain/validatorMonitor.js"; import { computePreFuluKzgCommitmentsInclusionProof, getBlobSidecars, kzgCommitmentToVersionedHash, reconstructBlobs, } from "../../../../util/blobs.js"; import { getBlobKzgCommitments, getDataColumnSidecarsFromBlock, getGloasDataColumnSidecars, } from "../../../../util/dataColumns.js"; import { isOptimisticBlock } from "../../../../util/forkChoice.js"; import { kzg } from "../../../../util/kzg.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 ({ signedBlockContents, broadcastValidation }, _context, opts = {}) => { const seenTimestampSec = Date.now() / 1000; const signedBlock = signedBlockContents.signedBlock; const slot = signedBlock.message.slot; const fork = config.getForkName(slot); const blockRoot = toRootHex(chain.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(signedBlock.message)); const blockForImport = chain.seenBlockInputCache.getByBlock({ block: signedBlock, source: BlockInputSource.api, seenTimestampSec, blockRootHex: blockRoot, }); if (isForkPostGloas(fork)) { chain.seenPayloadEnvelopeInputCache.add({ blockRootHex: blockRoot, block: signedBlock, forkName: fork, sampledColumns: chain.custodyConfig.sampledColumns, custodyColumns: chain.custodyConfig.custodyColumns, timeCreatedSec: seenTimestampSec, }); } let blobSidecars, dataColumnSidecars; if (isDenebBlockContents(signedBlockContents)) { if (isForkPostGloas(fork)) { // After gloas, data columns are not published with the block but when publishing the execution payload envelope blobSidecars = []; dataColumnSidecars = []; } else if (isForkPostFulu(fork)) { const timer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer(); // If the block was produced by this node, we will already have computed cells // Otherwise, we will compute them from the blobs in this function const cells = chain.blockProductionCache.get(blockRoot)?.cells ?? signedBlockContents.blobs.map((blob) => kzg.computeCells(blob)); const cellsAndProofs = cells.map((rowCells, rowIndex) => ({ cells: rowCells, proofs: signedBlockContents.kzgProofs.slice(rowIndex * NUMBER_OF_COLUMNS, (rowIndex + 1) * NUMBER_OF_COLUMNS), })); dataColumnSidecars = getDataColumnSidecarsFromBlock(config, signedBlock, cellsAndProofs); timer?.(); blobSidecars = []; } else if (isForkPostDeneb(fork)) { blobSidecars = getBlobSidecars(config, signedBlock, signedBlockContents.blobs, signedBlockContents.kzgProofs); dataColumnSidecars = []; } else { throw Error(`Invalid data fork=${fork} for publish`); } } else { blobSidecars = []; dataColumnSidecars = []; } if (isBlockInputColumns(blockForImport)) { for (const dataColumnSidecar of dataColumnSidecars) { blockForImport.addColumn({ blockRootHex: blockRoot, columnSidecar: dataColumnSidecar, source: BlockInputSource.api, seenTimestampSec, }, // In multi-BN setups (DVT, fallback), the same block may be published to multiple nodes. // Data columns may arrive via gossip from another node before the API publish completes, // so we allow duplicates here instead of throwing an error. { throwOnDuplicateAdd: false }); } } else if (isBlockInputBlobs(blockForImport)) { for (const blobSidecar of blobSidecars) { blockForImport.addBlob({ blockRootHex: blockRoot, blobSidecar, source: BlockInputSource.api, seenTimestampSec, }, // Same as above for columns { throwOnDuplicateAdd: false }); } } // 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 // bodyRoot should be the same to produced block const bodyRoot = toRootHex(chain.config.getForkTypes(slot).BeaconBlockBody.hashTreeRoot(signedBlock.message.body)); const blockLocallyProduced = chain.blockProductionCache.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.getBlockDefaultStatus(signedBlock.message.parentRoot); if (parentBlock === null) { chain.emitter.emit(ChainEvent.blockUnknownParent, { blockInput: blockForImport, peer: IDENTITY_PEER_ID, source: BlockInputSource.api, }); 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], null, { ...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, 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 - computeTimeAtSlot(config, slot, chain.genesisTime); metrics?.gossipBlock.elapsedTimeTillReceived.observe({ source: OpSource.api }, delaySec); chain.validatorMonitor?.registerBeaconBlock(OpSource.api, delaySec, signedBlock.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 behavior. // // - Publish blobs and block before importing so that network can see them asap // - Publish block first because // a) as soon as node sees block they can start processing it while data is in transit // b) getting block first allows nodes to use getBlobs from local ELs and save // import latency and hopefully bandwidth // () => network.publishBeaconBlock(signedBlock), ...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)), ...blobSidecars.map((blobSidecar) => () => network.publishBlobSidecar(blobSidecar)), () => // there is no rush to persist block since we published it to gossip anyway chain .processBlock(blockForImport, opts) .catch((e) => { if (e instanceof BlockError && (e.type.code === BlockErrorCode.PARENT_UNKNOWN || e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN)) { chain.emitter.emit(ChainEvent.blockUnknownParent, { blockInput: blockForImport, peer: IDENTITY_PEER_ID, source: BlockInputSource.api, }); } throw e; }), ]; const sentPeersArr = await promiseAllMaybeAsync(publishPromises); if (isForkPostGloas(fork)) { // After gloas, data columns are not published with the block but when publishing the execution payload envelope } else if (isForkPostFulu(fork)) { let columnsPublishedWithZeroPeers = 0; // sent peers per topic are logged in network.publishGossip(), here we only track metrics for it // starting from fulu, we have to push to 128 subnets so need to make sure we have enough sent peers per topic // + 1 because we publish to beacon_block first for (let i = 0; i < dataColumnSidecars.length; i++) { // + 1 because we publish to beacon_block first const sentPeers = sentPeersArr[i + 1]; // sent peers could be 0 as we set `allowPublishToZeroTopicPeers=true` in network.publishDataColumnSidecar() api metrics?.dataColumns.sentPeersPerSubnet.observe(sentPeers); if (sentPeers === 0) { columnsPublishedWithZeroPeers++; } } if (columnsPublishedWithZeroPeers > 0) { chain.logger.warn("Published data columns to 0 peers, increased risk of reorg", { slot, blockRoot, columns: columnsPublishedWithZeroPeers, }); } } chain.emitter.emit(routes.events.EventType.blockGossip, { slot, block: blockRoot }); if (isBlockInputColumns(blockForImport)) { const dataColumns = blockForImport.getAllColumns(); metrics?.dataColumns.bySource.inc({ source: BlockInputSource.api }, dataColumns.length); if (chain.emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) { for (const dataColumnSidecar of dataColumns) { chain.emitter.emit(routes.events.EventType.dataColumnSidecar, { blockRoot, slot, index: dataColumnSidecar.index, kzgCommitments: dataColumnSidecar.kzgCommitments.map(toHex), }); } } } else if (isBlockInputBlobs(blockForImport) && chain.emitter.listenerCount(routes.events.EventType.blobSidecar)) { const blobSidecars = blockForImport.getBlobs(); const versionedHashes = blockForImport.getVersionedHashes(); for (const blobSidecar of blobSidecars) { const { index, kzgCommitment } = blobSidecar; chain.emitter.emit(routes.events.EventType.blobSidecar, { blockRoot, slot, index, kzgCommitment: toHex(kzgCommitment), versionedHash: toHex(versionedHashes[index]), }); } } }; 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); if (isForkPostGloas(fork)) { throw new ApiError(400, `Blinded blocks are not available for post-gloas fork=${fork}`); } // Either the payload/blobs are cached from i) engine locally or ii) they are from the builder const producedResult = chain.blockProductionCache.get(blockRoot); if (producedResult !== undefined && producedResult.type !== BlockType.Blinded) { const source = ProducedBlockSource.engine; chain.logger.debug("Reconstructing the full signed block contents", { slot, blockRoot, source }); const signedBlockContents = reconstructSignedBlockContents(fork, signedBlindedBlock, producedResult.executionPayload ?? null, producedResult.blobsBundle ?? null); chain.logger.info("Publishing assembled block", { slot, blockRoot, source }); return publishBlock({ signedBlockContents }, { ...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 full signed block contents", { slot, blockRoot, source }); const signedBlockContents = await reconstructBuilderSignedBlockContents(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({ signedBlockContents }, { ...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 blockResult = await chain.getBlockByRoot(summary.blockRoot); if (blockResult) { const canonical = chain.forkChoice.getCanonicalBlockAtSlot(blockResult.block.message.slot); if (canonical) { result.push(toBeaconHeaderResponse(config, blockResult.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 blockResult = await chain.getBlockByRoot(summary.blockRoot); if (blockResult) { result.push(toBeaconHeaderResponse(config, blockResult.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); if (isForkPostGloas(fork)) { throw new ApiError(400, `Blinded blocks are not available for post-gloas fork=${fork}`); } 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.getBlockRootAtSlot(slot) }, 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 publishExecutionPayloadEnvelope({ signedExecutionPayloadEnvelope }) { const seenTimestampSec = Date.now() / 1000; const envelope = signedExecutionPayloadEnvelope.message; const slot = envelope.payload.slotNumber; const fork = config.getForkName(slot); const blockRootHex = toRootHex(envelope.beaconBlockRoot); const blockHashHex = toRootHex(envelope.payload.blockHash); if (!isForkPostGloas(fork)) { throw new ApiError(400, `publishExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`); } // TODO GLOAS: review checks, do we want to implement `broadcast_validation`? const block = chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.EMPTY); if (block === null) { throw new ApiError(404, `Block not found for beacon block root ${blockRootHex}`); } if (block.slot !== slot) { throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`); } await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope); const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; let dataColumnSidecars = []; if (isSelfBuild) { // For self-builds, construct and publish data column sidecars from cached block production data const cachedResult = chain.blockProductionCache.get(blockRootHex); if (cachedResult === undefined) { throw new ApiError(404, `No cached block production result found for block root ${blockRootHex}`); } if (!isForkPostGloas(cachedResult.fork)) { throw new ApiError(400, `Cached block production result is for pre-gloas fork=${cachedResult.fork}`); } if (cachedResult.type !== BlockType.Full) { throw new ApiError(400, "Cached block production result is not full block"); } if (cachedResult.cells && cachedResult.blobsBundle.commitments.length > 0) { const timer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer(); const cellsAndProofs = cachedResult.cells.map((rowCells, rowIndex) => ({ cells: rowCells, proofs: cachedResult.blobsBundle.proofs.slice(rowIndex * NUMBER_OF_COLUMNS, (rowIndex + 1) * NUMBER_OF_COLUMNS), })); dataColumnSidecars = getGloasDataColumnSidecars(slot, envelope.beaconBlockRoot, cellsAndProofs); timer?.(); } } else { // TODO GLOAS: will this api be used by builders or only for self-building? } // If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N. const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now(); if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) { await sleep(msToBlockSlot); } // TODO GLOAS: if block and payload are submitted in parallel, payloadInput may not yet exist. // A queuing mechanism is needed to handle this case. See https://github.com/ChainSafe/lodestar/issues/8915 const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); if (!payloadInput) { throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`); } payloadInput.addPayloadEnvelope({ envelope: signedExecutionPayloadEnvelope, source: PayloadEnvelopeInputSource.api, seenTimestampSec, peerIdStr: undefined, }); if (dataColumnSidecars.length > 0) { for (const columnSidecar of dataColumnSidecars) { payloadInput.addColumn({ columnSidecar, source: PayloadEnvelopeInputSource.api, seenTimestampSec, peerIdStr: undefined, }); } } const valLogMeta = { slot, blockRoot: blockRootHex, blockHash: blockHashHex, builderIndex: envelope.builderIndex, isSelfBuild, dataColumns: dataColumnSidecars.length, }; const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime); metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({ source: OpSource.api }, delaySec); chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.api, delaySec, signedExecutionPayloadEnvelope); chain.logger.info("Publishing execution payload envelope", valLogMeta); const publishPromises = [ // Gossip the signed execution payload envelope first () => network.publishSignedExecutionPayloadEnvelope(signedExecutionPayloadEnvelope), // For self-builds, publish all data column sidecars ...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)), // Import execution payload. Signature already verified above () => chain.processExecutionPayload(payloadInput, { validSignature: true }), ]; const publishPromise = promiseAllMaybeAsync(publishPromises); chain.emitter.emit(routes.events.EventType.executionPayloadGossip, { slot, builderIndex: envelope.builderIndex, blockHash: blockHashHex, blockRoot: blockRootHex, }); const sentPeersArr = await publishPromise; // Track metrics for data column publishing if (dataColumnSidecars.length > 0) { let columnsPublishedWithZeroPeers = 0; // Skip first entry (envelope); the final entry is processExecutionPayload(), which returns void. for (let i = 0; i < dataColumnSidecars.length; i++) { const sentPeers = sentPeersArr[i + 1]; metrics?.dataColumns.sentPeersPerSubnet.observe(sentPeers); if (sentPeers === 0) { columnsPublishedWithZeroPeers++; } } if (columnsPublishedWithZeroPeers > 0) { chain.logger.warn("Published data columns to 0 peers, increased risk of reorg", { slot, blockRoot: blockRootHex, columns: columnsPublishedWithZeroPeers, }); } metrics?.dataColumns.bySource.inc({ source: BlockInputSource.api }, dataColumnSidecars.length); if (chain.emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) { for (const dataColumnSidecar of dataColumnSidecars) { chain.emitter.emit(routes.events.EventType.dataColumnSidecar, { blockRoot: blockRootHex, slot, index: dataColumnSidecar.index, }); } } } chain.logger.info("Published execution payload envelope", { ...valLogMeta, delaySec, sentPeers: sentPeersArr[0] ?? 0, }); }, async getSignedExecutionPayloadEnvelope({ blockId }, context) { const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); const slot = block.message.slot; const fork = config.getForkName(slot); if (!isForkPostGloas(fork)) { throw new ApiError(400, `Execution payload envelopes are not available for pre-gloas fork=${fork}, slot=${slot}`); } const blockRoot = config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message); const blockRootHex = toRootHex(blockRoot); const data = context?.returnBytes ? await chain.getSerializedExecutionPayloadEnvelope(slot, blockRootHex) : await chain.getExecutionPayloadEnvelope(slot, blockRootHex); if (!data) { throw new ApiError(404, `Execution payload envelope not found for slot=${slot}, blockRoot=${blockRootHex}`); } return { data, meta: { executionOptimistic, finalized, version: fork }, }; }, async getBlobSidecars({ blockId, indices }) { assertUniqueItems(indices, "Duplicate indices provided"); const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); const fork = config.getForkName(block.message.slot); const blockRoot = sszTypesFor(fork).BeaconBlock.hashTreeRoot(block.message); const blockRootHex = toRootHex(blockRoot); let data; if (isForkPostFulu(fork)) { const { targetCustodyGroupCount } = chain.custodyConfig; if (targetCustodyGroupCount < NUMBER_OF_COLUMNS / 2) { throw new ApiError(503, `Custody group count of ${targetCustodyGroupCount} is not sufficient to serve blob sidecars, must custody at least ${NUMBER_OF_COLUMNS / 2} data columns`); } const blobKzgCommitments = block.message.body.blobKzgCommitments; const blobCount = blobKzgCommitments.length; if (blobCount > 0) { const dataColumnSidecars = await chain.getDataColumnSidecars(block.message.slot, blockRootHex); if (dataColumnSidecars.length === 0) { throw new ApiError(404, `dataColumnSidecars not found in db for slot=${block.message.slot} root=${toRootHex(blockRoot)} blobs=${blobCount}`); } for (const index of indices ?? []) { if (index < 0 || index >= blobCount) { throw new ApiError(400, `Invalid blob index ${index}, must be between 0 and ${blobCount - 1}`); } } const indicesToReconstruct = indices ?? Array.from({ length: blobCount }, (_, i) => i); const timer = metrics?.recoverBlobSidecars.reconstructionTime.startTimer(); const blobs = await reconstructBlobs(dataColumnSidecars, indicesToReconstruct); timer?.(); metrics?.recoverBlobSidecars.blobsReconstructed.inc(indicesToReconstruct.length); const signedBlockHeader = signedBlockToSignedHeader(config, block); data = await Promise.all(indicesToReconstruct.map(async (index, i) => { // record per column computation time const compTimer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer(); // Reconstruct blob sidecar from blob const kzgCommitment = blobKzgCommitments[index]; const blob = blobs[i]; // Use i since blobs only contains requested indices const kzgProof = await kzg.asyncComputeBlobKzgProof(blob, kzgCommitment); const kzgCommitmentInclusionProof = computePreFuluKzgCommitmentsInclusionProof(fork, block.message.body, index); compTimer?.(); return { index, blob, kzgCommitment, kzgProof, signedBlockHeader, kzgCommitmentInclusionProof }; })); } else { data = []; } } else if (isForkPostDeneb(fork)) { const blobSidecars = await chain.getBlobSidecars(block.message.slot, blockRootHex); if (!blobSidecars) { throw new ApiError(404, `blobSidecars not found in db for slot=${block.message.slot} root=${toRootHex(blockRoot)}`); } data = indices ? blobSidecars.filter(({ index }) => indices.includes(index)) : blobSidecars; } else { data = []; } return { data, meta: { executionOptimistic, finalized, version: config.getForkName(block.message.slot), }, }; }, async getBlobs({ blockId, versionedHashes }) { assertUniqueItems(versionedHashes, "Duplicate versioned hashes provided"); const { block, executionOptimistic, finalized } = await getBlockResponse(chain, blockId); const fork = config.getForkName(block.message.slot); const blockRoot = sszTypesFor(fork).BeaconBlock.hashTreeRoot(block.message); const blockRootHex = toRootHex(blockRoot); let blobs; if (isForkPostFulu(fork)) { const { targetCustodyGroupCount } = chain.custodyConfig; if (targetCustodyGroupCount < NUMBER_OF_COLUMNS / 2) { throw new ApiError(503, `Custody group count of ${targetCustodyGroupCount} is not sufficient to serve blobs, must custody at least ${NUMBER_OF_COLUMNS / 2} data columns`); } const blobKzgCommitments = getBlobKzgCommitments(fork, block); const blobCount = blobKzgCommitments.length; if (blobCount > 0) { const dataColumnSidecars = await chain.getDataColumnSidecars(block.message.slot, blockRootHex); if (dataColumnSidecars.length === 0) { throw new ApiError(404, `dataColumnSidecars not found in db for slot=${block.message.slot} root=${toRootHex(blockRoot)} blobs=${blobCount}`); } let indicesToReconstruct; if (versionedHashes) { const blockVersionedHashes = blobKzgCommitments.map((commitment) => toHex(kzgCommitmentToVersionedHash(commitment))); indicesToReconstruct = []; for (const requestedHash of versionedHashes) { const index = blockVersionedHashes.findIndex((hash) => hash === requestedHash); if (index === -1) { throw new ApiError(400, `Versioned hash ${requestedHash} not found in block`); } indicesToReconstruct.push(index); } indicesToReconstruct.sort((a, b) => a - b); } else { indicesToReconstruct = Array.from({ length: blobCount }, (_, i) => i); } const timer = metrics?.peerDas.dataColumnsReconstructionTime.startTimer(); blobs = await reconstructBlobs(dataColumnSidecars, indicesToReconstruct); timer?.(); metrics?.peerDas.reconstructedColumns.inc(indicesToReconstruct.length); } else { blobs = []; } } else if (isForkPostDeneb(fork)) { const blobSidecars = await chain.getBlobSidecars(block.message.slot, blockRootHex); if (!blobSidecars) { throw new ApiError(404, `blobSidecars not found in db for slot=${block.message.slot} root=${toRootHex(blockRoot)}`); } blobs = blobSidecars.sort((a, b) => a.index - b.index).map(({ blob }) => blob); if (blobs.length && versionedHashes) { const kzgCommitments = block.message.body.blobKzgCommitments; const blockVersionedHashes = kzgCommitments.map((commitment) => toHex(kzgCommitmentToVersionedHash(commitment))); const requestedIndices = []; for (const requestedHash of versionedHashes) { const index = blockVersionedHashes.findIndex((hash) => hash === requestedHash); if (index === -1) { throw new ApiError(400, `Versioned hash ${requestedHash} not found in block`); } requestedIndices.push(index); } blobs = requestedIndices.sort((a, b) => a - b).map((index) => blobs[index]); } } else { blobs = []; } return { data: blobs, meta: { executionOptimistic, finalized, }, }; }, }; } async function reconstructBuilderSignedBlockContents(chain, signedBlindedBlock) { const executionBuilder = chain.executionBuilder; if (!executionBuilder) { throw Error("executionBuilder required to publish SignedBlindedBeaconBlock"); } return executionBuilder.submitBlindedBlock(signedBlindedBlock); } 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