@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
554 lines (517 loc) • 17.5 kB
text/typescript
import {routes} from "@lodestar/api";
import {ChainForkConfig} from "@lodestar/config";
import {
ForkPostDeneb,
ForkPostFulu,
ForkPostGloas,
ForkPreFulu,
isForkPostDeneb,
isForkPostFulu,
isForkPostGloas,
} from "@lodestar/params";
import {BlobIndex, ColumnIndex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types";
import {LodestarError, byteArrayEquals, fromHex, prettyPrintIndices, toHex, toRootHex} from "@lodestar/utils";
import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
import {BlockInputSource, IBlockInput} from "../../chain/blocks/blockInput/types.js";
import {ChainEventEmitter} from "../../chain/emitter.js";
import {IBeaconChain} from "../../chain/interface.js";
import {validateBlockBlobSidecars} from "../../chain/validation/blobSidecar.js";
import {validateFuluBlockDataColumnSidecars} from "../../chain/validation/dataColumnSidecar.js";
import {INetwork} from "../../network/interface.js";
import {PeerSyncMeta} from "../../network/peers/peersData.js";
import {prettyPrintPeerIdStr} from "../../network/util.js";
import {getBlobKzgCommitments} from "../../util/dataColumns.js";
import {PeerIdStr} from "../../util/peerId.js";
import {WarnResult} from "../../util/wrapError.js";
import {
BlockInputSyncCacheItem,
PendingBlockInput,
PendingBlockInputStatus,
getBlockInputSyncCacheItemRootHex,
isPendingBlockInput,
} from "../types.js";
export type FetchByRootCoreProps = {
config: ChainForkConfig;
chain: IBeaconChain | null; // null for testing purposes
network: INetwork;
peerMeta: PeerSyncMeta;
};
export type FetchByRootProps = FetchByRootCoreProps & {
cacheItem: BlockInputSyncCacheItem;
blockRoot: Uint8Array;
};
export type FetchByRootAndValidateBlockProps = Omit<FetchByRootCoreProps, "peerMeta"> & {
peerIdStr: PeerIdStr;
blockRoot: Uint8Array;
};
export type FetchByRootAndValidateBlobsProps = FetchByRootAndValidateBlockProps & {
forkName: ForkPreFulu;
block: SignedBeaconBlock<ForkPostDeneb>;
blockRoot: Uint8Array;
missing: BlobIndex[];
};
export type FetchByRootAndValidateColumnsProps = FetchByRootCoreProps & {
blockRoot: Uint8Array;
forkName: ForkPostFulu;
block: SignedBeaconBlock<ForkPostFulu>;
missing: ColumnIndex[];
};
export type FetchByRootResponses = {
block: SignedBeaconBlock;
blobSidecars?: deneb.BlobSidecars;
columnSidecars?: fulu.DataColumnSidecar[];
};
export type DownloadByRootProps = FetchByRootCoreProps & {
cacheItem: BlockInputSyncCacheItem;
chain: IBeaconChain;
emitter: ChainEventEmitter;
};
export async function downloadByRoot({
config,
chain,
network,
emitter,
peerMeta,
cacheItem,
}: DownloadByRootProps): Promise<WarnResult<PendingBlockInput, DownloadByRootError>> {
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: IBlockInput;
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() as SignedBeaconBlock<ForkPostGloas>,
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: PendingBlockInputStatus;
let timeSyncedSec: number | undefined;
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,
}: FetchByRootProps): Promise<WarnResult<FetchByRootResponses, DownloadByRootError>> {
let block: SignedBeaconBlock;
let blobSidecars: deneb.BlobSidecars | undefined;
let columnSidecarResult: WarnResult<fulu.DataColumnSidecar[], DownloadByRootError> | undefined;
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 as ForkPreFulu,
block: block as SignedBeaconBlock<ForkPostDeneb>,
blockRoot,
missing: cacheItem.blockInput.getMissingBlobMeta().map(({index}) => index),
});
}
if (isBlockInputColumns(cacheItem.blockInput)) {
columnSidecarResult = await fetchAndValidateColumns({
config,
chain,
network,
peerMeta,
forkName: forkName as ForkPostFulu,
block: block as SignedBeaconBlock<ForkPostFulu>,
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 as SignedBeaconBlock<ForkPostFulu>,
missing: network.custodyConfig.sampledColumns,
});
} else if (isForkPostDeneb(forkName)) {
const commitments = (block as SignedBeaconBlock<ForkPostDeneb & ForkPreFulu>).message.body.blobKzgCommitments;
const blobCount = commitments.length;
blobSidecars = await fetchAndValidateBlobs({
config,
chain,
network,
peerIdStr,
forkName: forkName as ForkPreFulu,
blockRoot,
block: block as SignedBeaconBlock<ForkPostDeneb>,
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,
}: Omit<FetchByRootAndValidateBlockProps, "chain">): Promise<SignedBeaconBlock> {
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,
}: FetchByRootAndValidateBlobsProps): Promise<deneb.BlobSidecars> {
const blobSidecars: deneb.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 = [],
}: Pick<FetchByRootAndValidateBlobsProps, "network" | "peerIdStr" | "blockRoot" | "missing"> & {
indicesInPossession?: number[];
}): Promise<deneb.BlobSidecars> {
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,
}: FetchByRootAndValidateColumnsProps): Promise<WarnResult<fulu.DataColumnSidecar[], DownloadByRootError>> {
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},
])) as fulu.DataColumnSidecar[];
const warnings: DownloadByRootError[] = [];
// 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,
}: Pick<FetchByRootAndValidateColumnsProps, "network" | "peerMeta" | "blockRoot" | "missing">): Promise<
fulu.DataColumnSidecar[]
> {
return (await network.sendDataColumnSidecarsByRoot(peerMeta.peerId, [
{blockRoot, columns: missing},
])) as fulu.DataColumnSidecar[];
}
export enum DownloadByRootErrorCode {
MISMATCH_BLOCK_ROOT = "DOWNLOAD_BY_ROOT_ERROR_MISMATCH_BLOCK_ROOT",
EXTRA_SIDECAR_RECEIVED = "DOWNLOAD_BY_ROOT_ERROR_EXTRA_SIDECAR_RECEIVED",
NO_SIDECAR_RECEIVED = "DOWNLOAD_BY_ROOT_ERROR_NO_SIDECAR_RECEIVED",
NOT_ENOUGH_SIDECARS_RECEIVED = "DOWNLOAD_BY_ROOT_ERROR_NOT_ENOUGH_SIDECARS_RECEIVED",
INVALID_INCLUSION_PROOF = "DOWNLOAD_BY_ROOT_ERROR_INVALID_INCLUSION_PROOF",
INVALID_KZG_PROOF = "DOWNLOAD_BY_ROOT_ERROR_INVALID_KZG_PROOF",
MISSING_BLOCK_RESPONSE = "DOWNLOAD_BY_ROOT_ERROR_MISSING_BLOCK_RESPONSE",
MISSING_BLOB_RESPONSE = "DOWNLOAD_BY_ROOT_ERROR_MISSING_BLOB_RESPONSE",
MISSING_COLUMN_RESPONSE = "DOWNLOAD_BY_ROOT_ERROR_MISSING_COLUMN_RESPONSE",
Z = "DOWNLOAD_BY_ROOT_ERROR_Z",
}
export type DownloadByRootErrorType =
| {
code: DownloadByRootErrorCode.MISMATCH_BLOCK_ROOT;
peer: string;
requestedBlockRoot: string;
receivedBlockRoot: string;
}
| {
code: DownloadByRootErrorCode.EXTRA_SIDECAR_RECEIVED;
peer: string;
slot: Slot;
blockRoot: string;
invalidIndices: string;
}
| {
code: DownloadByRootErrorCode.NO_SIDECAR_RECEIVED;
peer: string;
slot: Slot;
blockRoot: string;
}
| {
code: DownloadByRootErrorCode.NOT_ENOUGH_SIDECARS_RECEIVED;
peer: string;
slot: Slot;
blockRoot: string;
missingIndices: string;
}
| {
code: DownloadByRootErrorCode.INVALID_INCLUSION_PROOF;
peer: string;
blockRoot: string;
sidecarIndex: number;
}
| {
code: DownloadByRootErrorCode.INVALID_KZG_PROOF;
peer: string;
blockRoot: string;
}
| {
code: DownloadByRootErrorCode.MISSING_BLOCK_RESPONSE;
peer: string;
blockRoot: string;
}
| {
code: DownloadByRootErrorCode.MISSING_BLOB_RESPONSE;
peer: string;
blockRoot: string;
}
| {
code: DownloadByRootErrorCode.MISSING_COLUMN_RESPONSE;
peer: string;
blockRoot: string;
};
export class DownloadByRootError extends LodestarError<DownloadByRootErrorType> {}