@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
824 lines • 46.2 kB
JavaScript
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