UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

351 lines • 15 kB
import { routes } from "@lodestar/api"; import { isForkPostDeneb, isForkPostFulu, isForkPostGloas, } from "@lodestar/params"; import { LodestarError, byteArrayEquals, fromHex, prettyPrintIndices, toHex, toRootHex } from "@lodestar/utils"; import { isBlockInputBlobs, isBlockInputColumns } from "../../chain/blocks/blockInput/blockInput.js"; import { BlockInputSource } from "../../chain/blocks/blockInput/types.js"; import { validateBlockBlobSidecars } from "../../chain/validation/blobSidecar.js"; import { validateFuluBlockDataColumnSidecars } from "../../chain/validation/dataColumnSidecar.js"; import { prettyPrintPeerIdStr } from "../../network/util.js"; import { getBlobKzgCommitments } from "../../util/dataColumns.js"; import { PendingBlockInputStatus, getBlockInputSyncCacheItemRootHex, isPendingBlockInput, } from "../types.js"; export async function downloadByRoot({ config, chain, network, emitter, peerMeta, cacheItem, }) { const rootHex = getBlockInputSyncCacheItemRootHex(cacheItem); const blockRoot = fromHex(rootHex); const { peerId: peerIdStr } = peerMeta; const { result: { block, blobSidecars, columnSidecars }, warnings, } = await fetchByRoot({ config, chain, network, cacheItem, blockRoot, peerMeta, }); let blockInput; if (isPendingBlockInput(cacheItem)) { blockInput = cacheItem.blockInput; if (!blockInput.hasBlock()) { blockInput.addBlock({ block, blockRootHex: rootHex, source: BlockInputSource.byRoot, seenTimestampSec: Date.now() / 1000, peerIdStr, }); } } else { blockInput = chain.seenBlockInputCache.getByBlock({ block, peerIdStr, blockRootHex: rootHex, seenTimestampSec: Date.now() / 1000, source: BlockInputSource.byRoot, }); } if (isForkPostGloas(blockInput.forkName)) { chain.seenPayloadEnvelopeInputCache.add({ blockRootHex: rootHex, block: blockInput.getBlock(), forkName: blockInput.forkName, sampledColumns: chain.custodyConfig.sampledColumns, custodyColumns: chain.custodyConfig.custodyColumns, timeCreatedSec: Date.now() / 1000, }); } const hasAllDataPreDownload = blockInput.hasBlockAndAllData(); if (isBlockInputBlobs(blockInput) && !hasAllDataPreDownload) { // blobSidecars could be undefined if gossip resulted in full block+blobs so we don't download any if (!blobSidecars) { throw new DownloadByRootError({ code: DownloadByRootErrorCode.MISSING_BLOB_RESPONSE, blockRoot: rootHex, peer: peerIdStr, }); } for (const blobSidecar of blobSidecars) { if (blockInput.hasBlob(blobSidecar.index)) { // the same BlobSidecar may be added by gossip while waiting for fetchByRoot // TODO(fulu): add metric here to track this continue; } blockInput.addBlob({ blobSidecar, blockRootHex: rootHex, seenTimestampSec: Date.now() / 1000, source: BlockInputSource.byRoot, peerIdStr, }); if (emitter.listenerCount(routes.events.EventType.blobSidecar)) { const versionedHashes = blockInput.getVersionedHashes(); emitter.emit(routes.events.EventType.blobSidecar, { blockRoot: rootHex, slot: blockInput.slot, index: blobSidecar.index, kzgCommitment: toHex(blobSidecar.kzgCommitment), versionedHash: toHex(versionedHashes[blobSidecar.index]), }); } } } if (isBlockInputColumns(blockInput) && !hasAllDataPreDownload) { // columnSidecars could be undefined if gossip resulted in full block+columns so we don't download any if (!columnSidecars) { throw new DownloadByRootError({ code: DownloadByRootErrorCode.MISSING_COLUMN_RESPONSE, blockRoot: rootHex, peer: peerIdStr, }); } for (const columnSidecar of columnSidecars) { if (blockInput.hasColumn(columnSidecar.index)) { // the same DataColumnSidecar may be added by gossip while waiting for fetchByRoot // TODO(fulu): add metric here to track this continue; } blockInput.addColumn({ columnSidecar, blockRootHex: rootHex, seenTimestampSec: Date.now() / 1000, source: BlockInputSource.byRoot, peerIdStr, }); if (emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) { emitter.emit(routes.events.EventType.dataColumnSidecar, { blockRoot: rootHex, slot: blockInput.slot, index: columnSidecar.index, kzgCommitments: columnSidecar.kzgCommitments.map(toHex), }); } } } let status; let timeSyncedSec; if (blockInput.hasBlockAndAllData()) { status = PendingBlockInputStatus.downloaded; timeSyncedSec = Date.now() / 1000; } else { status = PendingBlockInputStatus.pending; } return { result: { status, blockInput, timeSyncedSec, timeAddedSec: cacheItem.timeAddedSec, peerIdStrings: cacheItem.peerIdStrings, }, warnings, }; } export async function fetchByRoot({ config, chain, network, peerMeta, blockRoot, cacheItem, }) { let block; let blobSidecars; let columnSidecarResult; const { peerId: peerIdStr } = peerMeta; if (isPendingBlockInput(cacheItem)) { if (cacheItem.blockInput.hasBlock()) { block = cacheItem.blockInput.getBlock(); } else { block = await fetchAndValidateBlock({ config, network, peerIdStr, blockRoot, }); } const forkName = config.getForkName(block.message.slot); if (!cacheItem.blockInput.hasAllData()) { if (isBlockInputBlobs(cacheItem.blockInput)) { blobSidecars = await fetchAndValidateBlobs({ config, chain, network, peerIdStr, forkName: forkName, block: block, blockRoot, missing: cacheItem.blockInput.getMissingBlobMeta().map(({ index }) => index), }); } if (isBlockInputColumns(cacheItem.blockInput)) { columnSidecarResult = await fetchAndValidateColumns({ config, chain, network, peerMeta, forkName: forkName, block: block, blockRoot, missing: cacheItem.blockInput.getMissingSampledColumnMeta().missing, }); } } } else { block = await fetchAndValidateBlock({ config, network, peerIdStr, blockRoot, }); const forkName = config.getForkName(block.message.slot); if (isForkPostGloas(forkName)) { // Post-gloas block sync only needs the block body. Payload columns stay on the // payload/envelope path and are queued independently in the network processor. } else if (isForkPostFulu(forkName)) { columnSidecarResult = await fetchAndValidateColumns({ config, chain, network, peerMeta, forkName, blockRoot, block: block, missing: network.custodyConfig.sampledColumns, }); } else if (isForkPostDeneb(forkName)) { const commitments = block.message.body.blobKzgCommitments; const blobCount = commitments.length; blobSidecars = await fetchAndValidateBlobs({ config, chain, network, peerIdStr, forkName: forkName, blockRoot, block: block, missing: Array.from({ length: blobCount }, (_, i) => i), }); } } return { result: { block, blobSidecars, columnSidecars: columnSidecarResult?.result, }, warnings: columnSidecarResult?.warnings ?? null, }; } export async function fetchAndValidateBlock({ config, network, peerIdStr, blockRoot, }) { const response = await network.sendBeaconBlocksByRoot(peerIdStr, [blockRoot]); const block = response.at(0); if (!block) { throw new DownloadByRootError({ code: DownloadByRootErrorCode.MISSING_BLOCK_RESPONSE, peer: prettyPrintPeerIdStr(peerIdStr), blockRoot: toRootHex(blockRoot), }); } const receivedRoot = config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message); if (!byteArrayEquals(receivedRoot, blockRoot)) { throw new DownloadByRootError({ code: DownloadByRootErrorCode.MISMATCH_BLOCK_ROOT, peer: prettyPrintPeerIdStr(peerIdStr), requestedBlockRoot: toRootHex(blockRoot), receivedBlockRoot: toRootHex(receivedRoot), }, "block does not match requested root"); } return block; } export async function fetchAndValidateBlobs({ chain, network, peerIdStr, blockRoot, block, missing, }) { const blobSidecars = await fetchBlobsByRoot({ network, peerIdStr, blockRoot, missing, }); await validateBlockBlobSidecars(chain, block.message.slot, blockRoot, missing.length, blobSidecars); return blobSidecars; } export async function fetchBlobsByRoot({ network, peerIdStr, blockRoot, missing, indicesInPossession = [], }) { const blobsRequest = missing .filter((index) => !indicesInPossession.includes(index)) .map((index) => ({ blockRoot, index })); if (!blobsRequest.length) { return []; } return await network.sendBlobSidecarsByRoot(peerIdStr, blobsRequest); } export async function fetchAndValidateColumns({ chain, network, peerMeta, forkName, block, blockRoot, missing, }) { const { peerId: peerIdStr } = peerMeta; const slot = block.message.slot; const blobCount = getBlobKzgCommitments(forkName, block).length; if (blobCount === 0) { return { result: [], warnings: null }; } const blockRootHex = toRootHex(blockRoot); const peerColumns = new Set(peerMeta.custodyColumns ?? []); const requestedColumns = missing.filter((c) => peerColumns.has(c)); // TODO GLOAS: Extend by root column sync to support gloas.DataColumnSidecar and // validate against block bid commitments instead of the fulu signed header shape const columnSidecars = (await network.sendDataColumnSidecarsByRoot(peerIdStr, [ { blockRoot, columns: requestedColumns }, ])); const warnings = []; // it's not acceptable if no sidecar is returned with >0 blobCount if (columnSidecars.length === 0) { throw new DownloadByRootError({ code: DownloadByRootErrorCode.NO_SIDECAR_RECEIVED, peer: prettyPrintPeerIdStr(peerIdStr), slot, blockRoot: blockRootHex, }); } // it's ok if only some sidecars are returned, we will try to get the rest from other peers const requestedColumnsSet = new Set(requestedColumns); const returnedColumns = columnSidecars.map((c) => c.index); const returnedColumnsSet = new Set(returnedColumns); const missingIndices = requestedColumns.filter((c) => !returnedColumnsSet.has(c)); if (missingIndices.length > 0) { warnings.push(new DownloadByRootError({ code: DownloadByRootErrorCode.NOT_ENOUGH_SIDECARS_RECEIVED, peer: prettyPrintPeerIdStr(peerIdStr), slot, blockRoot: blockRootHex, missingIndices: prettyPrintIndices(missingIndices), }, "Did not receive all of the requested columnSidecars")); } // check extra returned columnSidecar const extraIndices = returnedColumns.filter((c) => !requestedColumnsSet.has(c)); if (extraIndices.length > 0) { warnings.push(new DownloadByRootError({ code: DownloadByRootErrorCode.EXTRA_SIDECAR_RECEIVED, peer: prettyPrintPeerIdStr(peerIdStr), slot, blockRoot: blockRootHex, invalidIndices: prettyPrintIndices(extraIndices), }, "Received columnSidecars that were not requested")); } // TODO GLOAS: Swap to fork-aware column validation once post-gloas by-root sync is implemented await validateFuluBlockDataColumnSidecars(chain, slot, blockRoot, blobCount, columnSidecars, chain?.metrics?.peerDas); return { result: columnSidecars, warnings: warnings.length > 0 ? warnings : null }; } // TODO(fulu) not in use, remove? export async function fetchColumnsByRoot({ network, peerMeta, blockRoot, missing, }) { return (await network.sendDataColumnSidecarsByRoot(peerMeta.peerId, [ { blockRoot, columns: missing }, ])); } export { DownloadByRootErrorCode }; var DownloadByRootErrorCode; (function (DownloadByRootErrorCode) { DownloadByRootErrorCode["MISMATCH_BLOCK_ROOT"] = "DOWNLOAD_BY_ROOT_ERROR_MISMATCH_BLOCK_ROOT"; DownloadByRootErrorCode["EXTRA_SIDECAR_RECEIVED"] = "DOWNLOAD_BY_ROOT_ERROR_EXTRA_SIDECAR_RECEIVED"; DownloadByRootErrorCode["NO_SIDECAR_RECEIVED"] = "DOWNLOAD_BY_ROOT_ERROR_NO_SIDECAR_RECEIVED"; DownloadByRootErrorCode["NOT_ENOUGH_SIDECARS_RECEIVED"] = "DOWNLOAD_BY_ROOT_ERROR_NOT_ENOUGH_SIDECARS_RECEIVED"; DownloadByRootErrorCode["INVALID_INCLUSION_PROOF"] = "DOWNLOAD_BY_ROOT_ERROR_INVALID_INCLUSION_PROOF"; DownloadByRootErrorCode["INVALID_KZG_PROOF"] = "DOWNLOAD_BY_ROOT_ERROR_INVALID_KZG_PROOF"; DownloadByRootErrorCode["MISSING_BLOCK_RESPONSE"] = "DOWNLOAD_BY_ROOT_ERROR_MISSING_BLOCK_RESPONSE"; DownloadByRootErrorCode["MISSING_BLOB_RESPONSE"] = "DOWNLOAD_BY_ROOT_ERROR_MISSING_BLOB_RESPONSE"; DownloadByRootErrorCode["MISSING_COLUMN_RESPONSE"] = "DOWNLOAD_BY_ROOT_ERROR_MISSING_COLUMN_RESPONSE"; DownloadByRootErrorCode["Z"] = "DOWNLOAD_BY_ROOT_ERROR_Z"; })(DownloadByRootErrorCode || (DownloadByRootErrorCode = {})); export class DownloadByRootError extends LodestarError { } //# sourceMappingURL=downloadByRoot.js.map