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