UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

768 lines 38.1 kB
import { isForkPostFulu, isForkPostGloas, } from "@lodestar/params"; import { isGloasDataColumnSidecar, } from "@lodestar/types"; import { LodestarError, byteArrayEquals, fromHex, prettyPrintIndices, toRootHex } from "@lodestar/utils"; import { BlockInputSource, DAType, isBlockInputBlobs, isBlockInputColumns, } from "../../chain/blocks/blockInput/index.js"; import { PayloadEnvelopeInputSource } from "../../chain/blocks/payloadEnvelopeInput/types.js"; import { validateBlockBlobSidecars } from "../../chain/validation/blobSidecar.js"; import { validateFuluBlockDataColumnSidecars, validateGloasBlockDataColumnSidecars, } from "../../chain/validation/dataColumnSidecar.js"; import { getBlobKzgCommitments } from "../../util/dataColumns.js"; /** * Given existing cached batch block inputs and newly validated responses, update the cache with the new data */ export function cacheByRangeResponses({ cache, seenPayloadEnvelopeInputCache, peerIdStr, responses, batchBlocks, downloadedPayloadEnvelopes, existingPayloadEnvelopes, custodyConfig, seenTimestampSec, }) { const source = BlockInputSource.byRange; const updatedBatchBlocks = new Map(batchBlocks.map((block) => [block.slot, block])); const blocks = responses.validatedBlocks ?? []; for (let i = 0; i < blocks.length; i++) { const { block, blockRoot } = blocks[i]; const blockRootHex = toRootHex(blockRoot); const existing = updatedBatchBlocks.get(block.message.slot); if (existing) { // In practice this code block shouldn't be reached because we shouldn't be refetching a block we already have, see Batch#getRequests. // Will throw if root hex does not match (meaning we are following the wrong chain) existing.addBlock({ block, blockRootHex, source, peerIdStr, seenTimestampSec, }, { throwOnDuplicateAdd: false }); } else { const blockInput = cache.getByBlock({ block, blockRootHex, source, peerIdStr, seenTimestampSec, }); updatedBatchBlocks.set(blockInput.slot, blockInput); } } for (const { blockRoot, blobSidecars } of responses.validatedBlobSidecars ?? []) { const dataSlot = blobSidecars.at(0)?.signedBlockHeader.message.slot; if (dataSlot === undefined) { throw new Error(`Coding Error: empty blobSidecars returned for blockRoot=${toRootHex(blockRoot)} from validation functions`); } const existing = updatedBatchBlocks.get(dataSlot); const blockRootHex = toRootHex(blockRoot); if (!existing) { throw new Error("Coding error: blockInput must exist when adding blobs"); } if (!isBlockInputBlobs(existing)) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISMATCH_BLOCK_INPUT_TYPE, slot: existing.slot, blockRoot: existing.blockRootHex, expected: DAType.Blobs, actual: existing.type, }); } for (const blobSidecar of blobSidecars) { // will throw if root hex does not match (meaning we are following the wrong chain) existing.addBlob({ blobSidecar, blockRootHex, seenTimestampSec, peerIdStr, source, }, { throwOnDuplicateAdd: false }); } } // Seed seenPayloadEnvelopeInputCache for every gloas block in the batch, regardless of whether // the peer returned its envelope. Without this, a block returned without its envelope would be // imported with no cache entry, and later payload-by-root sync would throw // "Missing PayloadEnvelopeInput for known block" (see issue #9306). for (const blockInput of updatedBatchBlocks.values()) { if (!blockInput.hasBlock() || !isForkPostGloas(blockInput.forkName)) continue; seenPayloadEnvelopeInputCache.add({ blockRootHex: blockInput.blockRootHex, block: blockInput.getBlock(), forkName: blockInput.forkName, sampledColumns: custodyConfig.sampledColumns, custodyColumns: custodyConfig.custodyColumns, timeCreatedSec: seenTimestampSec, }); } let payloadEnvelopes = existingPayloadEnvelopes !== null ? new Map(existingPayloadEnvelopes) : null; if (downloadedPayloadEnvelopes !== null) { payloadEnvelopes ??= new Map(); for (const [slot, envelope] of downloadedPayloadEnvelopes) { const envelopeBlockRootHex = toRootHex(envelope.message.beaconBlockRoot); const payloadInput = seenPayloadEnvelopeInputCache.get(envelopeBlockRootHex); if (payloadInput === undefined) { // Unreachable given the loop above seeded an entry for every gloas block in the batch. // for the parent block, it's populated at BeaconChain init throw new Error(`Missing PayloadEnvelopeInput for block ${envelopeBlockRootHex}`); } if (!payloadInput.hasPayloadEnvelope()) { payloadInput.addPayloadEnvelope({ envelope, source: PayloadEnvelopeInputSource.byRange, seenTimestampSec, peerIdStr, }); } payloadEnvelopes.set(slot, payloadInput); } } for (const { blockRoot, columnSidecars } of responses.validatedColumnSidecars ?? []) { const firstColumn = columnSidecars[0]; if (!firstColumn) { throw new Error(`Coding Error: empty columnSidecars returned for blockRoot=${toRootHex(blockRoot)} from validation functions`); } const blockRootHex = toRootHex(blockRoot); if (isGloasDataColumnSidecar(firstColumn)) { // Gloas columns are attached to the matching PayloadEnvelopeInput, NOT to IBlockInput. // Gloas DataColumnSidecar has `slot` directly (no signedBlockHeader). const dataSlot = firstColumn.slot; const payloadInput = payloadEnvelopes?.get(dataSlot); if (!payloadInput) { // Should not happen: we built payloadInputs for all gloas blocks above continue; } for (const columnSidecar of columnSidecars) { payloadInput.addColumn({ columnSidecar, seenTimestampSec, peerIdStr, source: PayloadEnvelopeInputSource.byRange, }); } continue; } const fuluColumns = columnSidecars; const dataSlot = fuluColumns[0].signedBlockHeader.message.slot; const existing = updatedBatchBlocks.get(dataSlot); if (!existing) { throw new Error("Coding error: blockInput must exist when adding columns"); } if (!isBlockInputColumns(existing)) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISMATCH_BLOCK_INPUT_TYPE, slot: existing.slot, blockRoot: existing.blockRootHex, expected: DAType.Columns, actual: existing.type, }); } for (const columnSidecar of fuluColumns) { // will throw if root hex does not match (meaning we are following the wrong chain) existing.addColumn({ columnSidecar, blockRootHex, seenTimestampSec, peerIdStr, source, }, { throwOnDuplicateAdd: false }); } } return { blocks: Array.from(updatedBatchBlocks.values()), payloadEnvelopes }; } export async function downloadByRange({ config, network, peerIdStr, batchBlocks, blocksRequest, blobsRequest, columnsRequest, envelopesRequest, parentPayloadRequest, parentPayloadCommitments, peerDasMetrics, }) { let response; try { response = await requestByRange({ network, peerIdStr, blocksRequest, blobsRequest, columnsRequest, envelopesRequest, parentPayloadRequest, }); } catch (err) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.REQ_RESP_ERROR, reason: err.message, ...requestsLogMeta({ blocksRequest, blobsRequest, columnsRequest }), }); } return validateResponses({ config, batchBlocks, blocksRequest, blobsRequest, columnsRequest, envelopesRequest, parentPayloadRequest, parentPayloadCommitments, peerDasMetrics, ...response, }); } /** * Should not be called directly. Only exported for unit testing purposes */ export async function requestByRange({ network, peerIdStr, blocksRequest, blobsRequest, columnsRequest, envelopesRequest, parentPayloadRequest, }) { let blocks; let blobSidecars; const columnSidecars = []; const payloadEnvelopes = []; const requests = []; if (blocksRequest) { requests.push(network.sendBeaconBlocksByRange(peerIdStr, blocksRequest).then((blockResponse) => { blocks = blockResponse; })); } if (blobsRequest) { requests.push(network.sendBlobSidecarsByRange(peerIdStr, blobsRequest).then((blobResponse) => { blobSidecars = blobResponse; })); } if (columnsRequest) { requests.push(network.sendDataColumnSidecarsByRange(peerIdStr, columnsRequest).then((columnResponse) => { columnSidecars.push(...columnResponse); })); } if (envelopesRequest) { requests.push(network.sendExecutionPayloadEnvelopesByRange(peerIdStr, envelopesRequest).then((envelopeResponse) => { payloadEnvelopes.push(...envelopeResponse); })); } // Only happens on the 1st batch of a SyncChain — fetches whichever pieces of the // dangling-parent's payload are still missing from the seen-cache entry. if (parentPayloadRequest?.envelopeBlockRoot) { requests.push(network .sendExecutionPayloadEnvelopesByRoot(peerIdStr, [parentPayloadRequest.envelopeBlockRoot]) .then((envelopeResponse) => { payloadEnvelopes.push(...envelopeResponse); })); } if (parentPayloadRequest?.blockRoot && parentPayloadRequest.columns && parentPayloadRequest.columns.length > 0) { requests.push(network .sendDataColumnSidecarsByRoot(peerIdStr, [ { blockRoot: parentPayloadRequest.blockRoot, columns: parentPayloadRequest.columns }, ]) .then((columnResponse) => { columnSidecars.push(...columnResponse); })); } await Promise.all(requests); return { blocks, blobSidecars, columnSidecars, payloadEnvelopes, }; } /** * Should not be called directly. Only exported for unit testing purposes */ export async function validateResponses({ config, batchBlocks, blocksRequest, blobsRequest, columnsRequest, envelopesRequest, parentPayloadRequest, parentPayloadCommitments, blocks, blobSidecars, columnSidecars, payloadEnvelopes, peerDasMetrics, }) { // Blocks are always required for blob/column validation // If a blocksRequest is provided, blocks have just been downloaded // If no blocksRequest is provided, batchBlocks must have been provided from cache if ((blobsRequest || columnsRequest) && !(blocks || batchBlocks)) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISSING_BLOCKS_RESPONSE, ...requestsLogMeta({ blobsRequest, columnsRequest }), }, "No blocks to validate data requests against"); } // `parentPayloadRequest` and `parentPayloadCommitments` must be supplied together if ((parentPayloadRequest === undefined) !== (parentPayloadCommitments === undefined)) { throw new Error("Coding error: parentPayloadRequest and parentPayloadCommitments must be both set or both unset"); } const validatedResponses = {}; let warnings = null; if (blocksRequest) { const result = validateBlockByRangeResponse(config, blocksRequest, blocks ?? []); if (result.warnings?.length) { warnings = result.warnings; } validatedResponses.validatedBlocks = result.result; } const needsEnvelopeValidation = !!envelopesRequest || parentPayloadCommitments !== undefined; const dataRequest = blobsRequest ?? columnsRequest; if (!dataRequest && !needsEnvelopeValidation) { return { result: { responses: validatedResponses, payloadEnvelopes: null }, warnings }; } if (!dataRequest) { // Only envelope and/or parent-by-root validation needed if (parentPayloadCommitments !== undefined) { const parentValidated = await validateParentPayloadColumns(parentPayloadCommitments, columnSidecars ?? [], peerDasMetrics); if (parentValidated) { validatedResponses.validatedColumnSidecars = [parentValidated]; } } const validatedPayloadEnvelopes = validateEnvelopesByRangeResponse(validatedResponses.validatedBlocks ?? [], batchBlocks, payloadEnvelopes ?? [], parentPayloadCommitments); return { result: { responses: validatedResponses, payloadEnvelopes: validatedPayloadEnvelopes }, warnings }; } const blocksForDataValidation = getBlocksForDataValidation(dataRequest, batchBlocks, validatedResponses.validatedBlocks?.length ? validatedResponses.validatedBlocks : undefined); if (!blocksForDataValidation.length) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISSING_BLOCKS_RESPONSE, ...requestsLogMeta({ blobsRequest, columnsRequest }), }, "No blocks in data request slot range to validate data response against"); } if (blobsRequest) { if (!blobSidecars) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISSING_BLOBS_RESPONSE, ...requestsLogMeta({ blobsRequest, columnsRequest }), }, "No blobSidecars to validate against blobsRequest"); } validatedResponses.validatedBlobSidecars = await validateBlobsByRangeResponse(blocksForDataValidation, blobSidecars); } if (columnsRequest) { if (!columnSidecars) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISSING_COLUMNS_RESPONSE, ...requestsLogMeta({ blobsRequest, columnsRequest }), }, "No columnSidecars to check columnRequest against"); } const validatedColumnSidecarsResult = await validateColumnsByRangeResponse(config, columnsRequest, blocksForDataValidation, columnSidecars, peerDasMetrics); validatedResponses.validatedColumnSidecars = validatedColumnSidecarsResult.result; warnings = validatedColumnSidecarsResult.warnings; } // Parent columns (by-root): KZG-validate against parent's bid commitments and append. if (parentPayloadCommitments !== undefined) { const parentValidated = await validateParentPayloadColumns(parentPayloadCommitments, columnSidecars ?? [], peerDasMetrics); if (parentValidated) { validatedResponses.validatedColumnSidecars = [ ...(validatedResponses.validatedColumnSidecars ?? []), parentValidated, ]; } } let validatedPayloadEnvelopes = null; if (needsEnvelopeValidation) { validatedPayloadEnvelopes = validateEnvelopesByRangeResponse(validatedResponses.validatedBlocks ?? [], batchBlocks, payloadEnvelopes ?? [], parentPayloadCommitments); } return { result: { responses: validatedResponses, payloadEnvelopes: validatedPayloadEnvelopes }, warnings }; } async function validateParentPayloadColumns(parentPayloadCommitments, columnSidecars, peerDasMetrics) { const parentColumns = []; for (const cs of columnSidecars) { if (isGloasDataColumnSidecar(cs) && byteArrayEquals(cs.beaconBlockRoot, parentPayloadCommitments.blockRoot)) { parentColumns.push(cs); } } if (parentColumns.length === 0) { return null; } parentColumns.sort((a, b) => a.index - b.index); const parentSlot = parentColumns[0].slot; await validateGloasBlockDataColumnSidecars(parentSlot, parentPayloadCommitments.blockRoot, parentPayloadCommitments.kzgCommitments, parentColumns, peerDasMetrics); return { blockRoot: parentPayloadCommitments.blockRoot, columnSidecars: parentColumns }; } /** * Should not be called directly. Only exported for unit testing purposes * * - check all slots are within range of startSlot (inclusive) through startSlot + count (exclusive) * - don't have more than count number of blocks * - slots are in ascending order * - must allow for skip slots * - check is a chain of blocks where via parentRoot matches hashTreeRoot of block before */ export function validateBlockByRangeResponse(config, blocksRequest, blocks) { const { startSlot, count } = blocksRequest; // An error was thrown here by @twoeths in #8150 but it breaks for epochs with 0 blocks during chain // liveness issues. See comment https://github.com/ChainSafe/lodestar/issues/8147#issuecomment-3246434697 // There are instances where clients return no blocks though. Need to monitor this via the warns to see // if what the correct behavior should be if (!blocks.length) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISSING_BLOCKS_RESPONSE, ...requestsLogMeta({ blocksRequest }), }); // TODO: this was causing deadlock again. need to come back and fix this so that its possible to process through // an empty epoch for periods with poor liveness // return { // result: [], // warnings: [ // new DownloadByRangeError({ // code: DownloadByRangeErrorCode.MISSING_BLOCKS_RESPONSE, // ...requestsLogMeta({blocksRequest}), // }), // ], // }; } if (blocks.length > count) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.EXTRA_BLOCKS, expected: count, actual: blocks.length - count, }, "Extra blocks received in BeaconBlocksByRange response"); } const lastValidSlot = startSlot + count - 1; for (let i = 0; i < blocks.length; i++) { const slot = blocks[i].message.slot; if (slot < startSlot || slot > lastValidSlot) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS, slot, }, "Blocks in response outside of requested slot range"); } // do not check for out of order on first block, and for subsequent blocks make sure that // the current block in a later slot than the one prior if (i !== 0 && slot <= blocks[i - 1].message.slot) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.OUT_OF_ORDER_BLOCKS, }, "Blocks out of order in BeaconBlocksByRange response"); } } // assumes all blocks are from the same fork. Batch only generated epoch-wise requests starting at slot // 0 of the epoch const type = config.getForkTypes(blocks[0].message.slot).BeaconBlock; const response = []; for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; const blockRoot = type.hashTreeRoot(block.message); response.push({ block, blockRoot }); if (i < blocks.length - 1) { // compare the block root against the next block's parent root const parentRoot = blocks[i + 1].message.parentRoot; if (!byteArrayEquals(blockRoot, parentRoot)) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH, slot: blocks[i].message.slot, expected: toRootHex(blockRoot), actual: toRootHex(parentRoot), }, `Block parent root does not match the previous block's root in BeaconBlocksByRange response`); } } } return { result: response, warnings: null, }; } /** * Should not be called directly. Only exported for unit testing purposes. * This is used only in Deneb and Electra */ export async function validateBlobsByRangeResponse(dataRequestBlocks, blobSidecars) { const expectedBlobCount = dataRequestBlocks.reduce((acc, { block }) => block.message.body.blobKzgCommitments.length + acc, 0); if (blobSidecars.length > expectedBlobCount) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.EXTRA_BLOBS, expected: expectedBlobCount, actual: blobSidecars.length, }, "Extra blobs received in BlobSidecarsByRange response"); } if (blobSidecars.length < expectedBlobCount) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISSING_BLOBS, expected: expectedBlobCount, actual: blobSidecars.length, }, "Missing blobs in BlobSidecarsByRange response"); } const validateSidecarsPromises = []; for (let blockIndex = 0, blobSidecarIndex = 0; blockIndex < dataRequestBlocks.length; blockIndex++) { const { block, blockRoot } = dataRequestBlocks[blockIndex]; const blockKzgCommitments = block.message.body .blobKzgCommitments; if (blockKzgCommitments.length === 0) { continue; } const blockBlobSidecars = blobSidecars.slice(blobSidecarIndex, blobSidecarIndex + blockKzgCommitments.length); blobSidecarIndex += blockKzgCommitments.length; for (let i = 0; i < blockBlobSidecars.length; i++) { if (blockBlobSidecars[i].index !== i) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.OUT_OF_ORDER_BLOBS, slot: block.message.slot, }, "Blob sidecars not in order or do not match expected indexes in BlobSidecarsByRange response"); } } validateSidecarsPromises.push(validateBlockBlobSidecars(null, // do not pass chain here so we do not validate header signature block.message.slot, blockRoot, blockKzgCommitments.length, blockBlobSidecars).then(() => ({ blockRoot, blobSidecars: blockBlobSidecars }))); } // Await all sidecar validations in parallel return Promise.all(validateSidecarsPromises); } /** * Should not be called directly. Only exported for unit testing purposes * * Spec states: * 1) must be within range [start_slot, start_slot + count] * 2) should respond with all columns in the range or and 3:ResourceUnavailable (and potentially get down-scored) * 3) must response with at least the sidecars of the first blob-carrying block that exists in the range * 4) must include all sidecars from each block from which there are blobs * 5) where they exists, sidecars must be sent in (slot, index) order * 6) clients may limit the number of sidecars in a response * 7) clients may stop responding mid-response if their view of fork-choice changes * * We will interpret the spec as follows * - Errors when validating: 1, 3, 5 * - Warnings when validating: 2, 4, 6, 7 * * For "warning" cases, where we get a partial response but sidecars are validated and correct with respect to the * blocks, then they will be kept. This loosening of the spec is to help ensure sync goes smoothly and we can find * the data needed in difficult network situations. * * Assume for the following two examples we request indices 5, 10, 15 for a range of slots 32-63 * * For slots where we receive no sidecars, example slot 45, but blobs exist we will stop validating subsequent * slots, 45-63. The next round of requests will get structured to pull the from the slot that had columns * missing to the end of the range for all columns indices that were requested for the current partially failed * request (slots 45-63 and indices 5, 10, 15). * * For slots where only some of the requested sidecars are received we will proceed with validation. For simplicity sake * we will assume that if we only get some indices back for a (or several) slot(s) that the indices we get will be * consistent. IE if a peer returns only index 5, they will most likely return that same index for subsequent slot * (index 5 for slots 34, 35, 36, etc). They will not likely return 5 on slot 34, 10 on slot 35, 15 on slot 36, etc. * This assumption makes the code simpler. For both cases the request for the next round will be structured correctly * to pull any missing column indices for whatever range remains. The simplification just leads to re-verification * of the columns but the number of columns downloaded will be the same regardless of if they are validated twice. * * validateColumnsByRangeResponse makes some assumptions about the data being passed in * blocks are: * - slotwise in order * - form a chain * - non-sparse response (any missing block is a skipped slot not a bad response) * - last block is last slot received */ export async function validateColumnsByRangeResponse(config, request, blocks, columnSidecars, peerDasMetrics) { const warnings = []; const seenColumns = new Map(); let currentSlot = -1; let currentIndex = -1; // Check for duplicates and order for (const columnSidecar of columnSidecars) { const slot = isGloasDataColumnSidecar(columnSidecar) ? columnSidecar.slot : columnSidecar.signedBlockHeader.message.slot; let seenSlotColumns = seenColumns.get(slot); if (!seenSlotColumns) { seenSlotColumns = new Map(); seenColumns.set(slot, seenSlotColumns); } if (seenSlotColumns.has(columnSidecar.index)) { warnings.push(new DownloadByRangeError({ code: DownloadByRangeErrorCode.DUPLICATE_COLUMN, slot, index: columnSidecar.index, })); continue; } if (currentSlot > slot) { warnings.push(new DownloadByRangeError({ code: DownloadByRangeErrorCode.OUT_OF_ORDER_COLUMNS, slot, }, "ColumnSidecars received out of slot order")); } if (currentSlot === slot && currentIndex > columnSidecar.index) { warnings.push(new DownloadByRangeError({ code: DownloadByRangeErrorCode.OUT_OF_ORDER_COLUMNS, slot, }, "Column indices out of order within a slot")); } seenSlotColumns.set(columnSidecar.index, columnSidecar); if (currentSlot !== slot) { // a new slot has started, reset index currentIndex = -1; } else { currentIndex = columnSidecar.index; } currentSlot = slot; } const validationPromises = []; for (const { blockRoot, block } of blocks) { const slot = block.message.slot; const rootHex = toRootHex(blockRoot); const forkName = config.getForkName(slot); const columnSidecarsMap = seenColumns.get(slot) ?? new Map(); const columnSidecars = Array.from(columnSidecarsMap.values()).sort((a, b) => a.index - b.index); let blobCount; if (!isForkPostFulu(forkName)) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISMATCH_BLOCK_FORK, slot, blockFork: forkName, dataFork: "unknown", }); } const kzgCommitments = getBlobKzgCommitments(forkName, block); blobCount = kzgCommitments.length; if (columnSidecars.length === 0) { if (!blobCount) { // no columns in the slot continue; } /** * If no columns are found for a block and there are commitments on the block then stop checking and just * return early. Even if there were columns returned for subsequent slots that doesn't matter because * we will be re-requesting them again anyway. Leftovers just get ignored */ warnings.push(new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISSING_COLUMNS, slot, blockRoot: rootHex, missingIndices: prettyPrintIndices(request.columns), })); break; } const returnedColumns = Array.from(columnSidecarsMap.keys()).sort(); if (!blobCount) { // columns for a block that does not have blobs // TODO(fulu): should this be a hard error with no data retained from peer or just a warning throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.NO_COLUMNS_FOR_BLOCK, slot, blockRoot: rootHex, invalidIndices: prettyPrintIndices(returnedColumns), }, "Block has no blob commitments but data column sidecars were provided"); } const missingIndices = request.columns.filter((i) => !columnSidecarsMap.has(i)); if (missingIndices.length > 0) { warnings.push(new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISSING_COLUMNS, slot, blockRoot: rootHex, missingIndices: prettyPrintIndices(missingIndices), }, "Missing data columns in DataColumnSidecarsByRange response")); } const extraIndices = returnedColumns.filter((i) => !request.columns.includes(i)); if (extraIndices.length > 0) { warnings.push(new DownloadByRangeError({ code: DownloadByRangeErrorCode.EXTRA_COLUMNS, slot, blockRoot: rootHex, invalidIndices: prettyPrintIndices(extraIndices), }, "Data column in not in requested columns in DataColumnSidecarsByRange response")); } const validatePromise = isForkPostGloas(forkName) ? validateGloasBlockDataColumnSidecars(slot, blockRoot, kzgCommitments, columnSidecars, peerDasMetrics) : validateFuluBlockDataColumnSidecars(null, // do not pass chain here so we do not validate header signature slot, blockRoot, blobCount, columnSidecars, peerDasMetrics); validationPromises.push(validatePromise.then(() => ({ blockRoot, columnSidecars, }))); } const validatedColumns = await Promise.all(validationPromises); return { result: validatedColumns, warnings: warnings.length ? warnings : null, }; } /** * Given a data request, return only the blocks and roots that correspond to the data request (sorted). Assumes that * cached have slots that are all before the current batch of downloaded blocks */ export function getBlocksForDataValidation(dataRequest, cached, current) { const startSlot = dataRequest.startSlot; const endSlot = startSlot + dataRequest.count; // Organize cached blocks and current blocks, only including those in the requested slot range const dataRequestBlocks = []; let lastSlot = startSlot - 1; if (cached) { for (let i = 0; i < cached.length; i++) { const blockInput = cached[i]; if (blockInput.slot >= startSlot && blockInput.slot < endSlot && blockInput.slot > lastSlot) { dataRequestBlocks.push({ block: blockInput.getBlock(), blockRoot: fromHex(blockInput.blockRootHex) }); lastSlot = blockInput.slot; } } } if (current) { for (let i = 0; i < current.length; i++) { const block = current[i].block; if (block.message.slot >= startSlot && block.message.slot < endSlot && block.message.slot > lastSlot) { dataRequestBlocks.push(current[i]); lastSlot = block.message.slot; } } } return dataRequestBlocks; } function requestsLogMeta({ blocksRequest, blobsRequest, columnsRequest }) { const logMeta = {}; if (blocksRequest) { logMeta.blockStartSlot = blocksRequest.startSlot; logMeta.blockCount = blocksRequest.count; } if (blobsRequest) { logMeta.blobStartSlot = blobsRequest.startSlot; logMeta.blobCount = blobsRequest.count; } if (columnsRequest) { logMeta.columnStartSlot = columnsRequest.startSlot; logMeta.columnCount = columnsRequest.count; } return logMeta; } export { DownloadByRangeErrorCode }; var DownloadByRangeErrorCode; (function (DownloadByRangeErrorCode) { DownloadByRangeErrorCode["MISSING_BLOCKS_RESPONSE"] = "DOWNLOAD_BY_RANGE_ERROR_MISSING_BLOCK_RESPONSE"; DownloadByRangeErrorCode["MISSING_BLOBS_RESPONSE"] = "DOWNLOAD_BY_RANGE_ERROR_MISSING_BLOBS_RESPONSE"; DownloadByRangeErrorCode["MISSING_COLUMNS_RESPONSE"] = "DOWNLOAD_BY_RANGE_ERROR_MISSING_COLUMNS_RESPONSE"; /** Error at the reqresp layer */ DownloadByRangeErrorCode["REQ_RESP_ERROR"] = "DOWNLOAD_BY_RANGE_ERROR_REQ_RESP_ERROR"; // Errors validating a chain of blocks (not considering associated data) DownloadByRangeErrorCode["PARENT_ROOT_MISMATCH"] = "DOWNLOAD_BY_RANGE_ERROR_PARENT_ROOT_MISMATCH"; DownloadByRangeErrorCode["EXTRA_BLOCKS"] = "DOWNLOAD_BY_RANGE_ERROR_EXTRA_BLOCKS"; DownloadByRangeErrorCode["OUT_OF_RANGE_BLOCKS"] = "DOWNLOAD_BY_RANGE_OUT_OF_RANGE_BLOCKS"; DownloadByRangeErrorCode["OUT_OF_ORDER_BLOCKS"] = "DOWNLOAD_BY_RANGE_OUT_OF_ORDER_BLOCKS"; DownloadByRangeErrorCode["MISSING_BLOBS"] = "DOWNLOAD_BY_RANGE_ERROR_MISSING_BLOBS"; DownloadByRangeErrorCode["OUT_OF_ORDER_BLOBS"] = "DOWNLOAD_BY_RANGE_ERROR_OUT_OF_ORDER_BLOBS"; DownloadByRangeErrorCode["EXTRA_BLOBS"] = "DOWNLOAD_BY_RANGE_ERROR_EXTRA_BLOBS"; DownloadByRangeErrorCode["MISSING_COLUMNS"] = "DOWNLOAD_BY_RANGE_ERROR_MISSING_COLUMNS"; DownloadByRangeErrorCode["OVER_COLUMNS"] = "DOWNLOAD_BY_RANGE_ERROR_OVER_COLUMNS"; DownloadByRangeErrorCode["EXTRA_COLUMNS"] = "DOWNLOAD_BY_RANGE_ERROR_EXTRA_COLUMNS"; DownloadByRangeErrorCode["NO_COLUMNS_FOR_BLOCK"] = "DOWNLOAD_BY_RANGE_ERROR_NO_COLUMNS_FOR_BLOCK"; DownloadByRangeErrorCode["DUPLICATE_COLUMN"] = "DOWNLOAD_BY_RANGE_ERROR_DUPLICATE_COLUMN"; DownloadByRangeErrorCode["OUT_OF_ORDER_COLUMNS"] = "DOWNLOAD_BY_RANGE_OUT_OF_ORDER_COLUMNS"; /** Cached block input type mismatches new data */ DownloadByRangeErrorCode["MISMATCH_BLOCK_FORK"] = "DOWNLOAD_BY_RANGE_ERROR_MISMATCH_BLOCK_FORK"; DownloadByRangeErrorCode["MISMATCH_BLOCK_INPUT_TYPE"] = "DOWNLOAD_BY_RANGE_ERROR_MISMATCH_BLOCK_INPUT_TYPE"; /** Envelope beaconBlockRoot does not match the block's root */ DownloadByRangeErrorCode["INVALID_ENVELOPE_BEACON_BLOCK_ROOT"] = "DOWNLOAD_BY_RANGE_ERROR_INVALID_ENVELOPE_BEACON_BLOCK_ROOT"; /** Block segment + envelopes failed chain-segment linearity / FULL-chain checks */ DownloadByRangeErrorCode["INVALID_CHAIN_SEGMENT"] = "DOWNLOAD_BY_RANGE_ERROR_INVALID_CHAIN_SEGMENT"; })(DownloadByRangeErrorCode || (DownloadByRangeErrorCode = {})); export class DownloadByRangeError extends LodestarError { } /** * Validates SignedExecutionPayloadEnvelopes received for a range request. * * Three categories of envelope slots: * - In-batch slot whose block we have: verify envelope.beaconBlockRoot matches. * - Dangling-parent envelope (only when `parentPayloadCommitments` is set, i.e. the first * batch of a `SyncChain`): keep if `envelope.beaconBlockRoot === parentPayloadCommitments.blockRoot`. * - Other "orphan" envelopes (e.g. unrelated slots): ignored. */ export function validateEnvelopesByRangeResponse(validatedBlocks, batchBlocks, payloadEnvelopes, parentPayloadCommitments) { // Build a map of slot -> blockRoot for all blocks in the batch const batchBlockRoots = new Map(); if (batchBlocks) { for (const blockInput of batchBlocks) { batchBlockRoots.set(blockInput.slot, fromHex(blockInput.blockRootHex)); } } for (const { block, blockRoot } of validatedBlocks) { batchBlockRoots.set(block.message.slot, blockRoot); } const payloadEnvelopeMap = new Map(); for (const payloadEnvelope of payloadEnvelopes) { const slot = payloadEnvelope.message.payload.slotNumber; const batchBlockRoot = batchBlockRoots.get(slot); if (batchBlockRoot === undefined) { // Keep the requested dangling-parent envelope only when its beaconBlockRoot matches // exactly. All other unrelated envelopes are dropped. if (parentPayloadCommitments !== undefined && byteArrayEquals(payloadEnvelope.message.beaconBlockRoot, parentPayloadCommitments.blockRoot)) { payloadEnvelopeMap.set(slot, payloadEnvelope); } continue; } // Verify beaconBlockRoot matches the block's root if (!byteArrayEquals(payloadEnvelope.message.beaconBlockRoot, batchBlockRoot)) { throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT, slot, expected: toRootHex(batchBlockRoot), actual: toRootHex(payloadEnvelope.message.beaconBlockRoot), }); } payloadEnvelopeMap.set(slot, payloadEnvelope); } return payloadEnvelopeMap; } //# sourceMappingURL=downloadByRange.js.map