UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

538 lines • 21.5 kB
import { fromHex, prettyBytes, toRootHex, withTimeout } from "@lodestar/utils"; import { kzgCommitmentToVersionedHash } from "../../../util/blobs.js"; import { BlockInputError, BlockInputErrorCode } from "./errors.js"; import { DAType, } from "./types.js"; export function isBlockInputPreDeneb(blockInput) { return blockInput.type === DAType.PreData; } export function isBlockInputBlobs(blockInput) { return blockInput.type === DAType.Blobs; } export function isBlockInputColumns(blockInput) { return blockInput.type === DAType.Columns; } function createPromise() { let resolve; let reject; const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); return { promise, resolve, reject, }; } class AbstractBlockInput { constructor(init) { this.blockPromise = createPromise(); this.dataPromise = createPromise(); this.daOutOfRange = init.daOutOfRange; this.timeCreatedSec = init.timeCreated; this.forkName = init.forkName; this.slot = init.slot; this.blockRootHex = init.blockRootHex; this.parentRootHex = init.parentRootHex; } hasBlock() { return this.state.hasBlock; } getBlock() { if (!this.state.hasBlock) { throw new BlockInputError({ code: BlockInputErrorCode.MISSING_BLOCK, blockRoot: this.blockRootHex, }, "Cannot getBlock from BlockInput without a block"); } return this.state.block; } getBlockSource() { if (!this.state.hasBlock) { throw new BlockInputError({ code: BlockInputErrorCode.MISSING_BLOCK, blockRoot: this.blockRootHex, }, "Cannot getBlockSource from BlockInput without a block"); } return this.state.source; } hasAllData() { return this.state.hasAllData; } hasBlockAndAllData() { return this.state.hasBlock && this.state.hasAllData; } getLogMeta() { return { blockRoot: prettyBytes(this.blockRootHex), slot: this.slot, timeCreatedSec: this.timeCreatedSec, }; } getTimeComplete() { if (!this.state.hasBlock || !this.state.hasAllData) { throw new BlockInputError({ code: BlockInputErrorCode.MISSING_TIME_COMPLETE, blockRoot: this.blockRootHex, }, "Cannot getTimeComplete from BlockInput without a block and data"); } return this.state.timeCompleteSec; } waitForBlock(timeout, signal) { if (!this.state.hasBlock) { return withTimeout(() => this.blockPromise.promise, timeout, signal); } return Promise.resolve(this.state.block); } waitForAllData(timeout, signal) { return withTimeout(() => this.dataPromise.promise, timeout, signal); } async waitForBlockAndAllData(timeout, signal) { if (!this.state.hasBlock || !this.state.hasAllData) { await withTimeout(() => Promise.all([this.blockPromise.promise, this.dataPromise.promise]), timeout, signal); } return this; } } /** * Pre-DA, BlockInput only has a single state. * - the block simply exists */ export class BlockInputPreData extends AbstractBlockInput { constructor(init, state) { super(init); this.type = DAType.PreData; this.state = state; } static createFromBlock(props) { const init = { daOutOfRange: props.daOutOfRange, timeCreated: props.source.seenTimestampSec, forkName: props.forkName, slot: props.block.message.slot, blockRootHex: props.blockRootHex, parentRootHex: toRootHex(props.block.message.parentRoot), }; const state = { hasBlock: true, hasAllData: true, block: props.block, source: props.source, timeCompleteSec: props.source.seenTimestampSec, }; return new BlockInputPreData(init, state); } addBlock(_) { throw new BlockInputError({ code: BlockInputErrorCode.INVALID_CONSTRUCTION, blockRoot: this.blockRootHex, }, "Cannot addBlock to BlockInputPreData"); } } /** * With blobs, BlockInput has several states: * - The block is seen and all blobs are seen * - The block is seen and all blobs are not yet seen * - The block is yet not seen and its unknown if all blobs are seen */ export class BlockInputBlobs extends AbstractBlockInput { constructor(init, state) { super(init); this.type = DAType.Blobs; this.blobsCache = new Map(); this.state = state; } static createFromBlock(props) { const hasAllData = props.daOutOfRange || props.block.message.body.blobKzgCommitments.length === 0; const state = { hasBlock: true, hasAllData, versionedHashes: props.block.message.body.blobKzgCommitments.map(kzgCommitmentToVersionedHash), block: props.block, source: props.source, timeCompleteSec: hasAllData ? props.source.seenTimestampSec : undefined, }; const init = { daOutOfRange: props.daOutOfRange, timeCreated: props.source.seenTimestampSec, forkName: props.forkName, slot: props.block.message.slot, blockRootHex: props.blockRootHex, parentRootHex: toRootHex(props.block.message.parentRoot), }; const blockInput = new BlockInputBlobs(init, state); blockInput.blockPromise.resolve(props.block); if (hasAllData) { blockInput.dataPromise.resolve([]); } return blockInput; } static createFromBlob(props) { const state = { hasBlock: false, hasAllData: false, }; const init = { daOutOfRange: props.daOutOfRange, timeCreated: props.seenTimestampSec, forkName: props.forkName, blockRootHex: props.blockRootHex, parentRootHex: toRootHex(props.blobSidecar.signedBlockHeader.message.parentRoot), slot: props.blobSidecar.signedBlockHeader.message.slot, }; const blockInput = new BlockInputBlobs(init, state); blockInput.blobsCache.set(props.blobSidecar.index, { blobSidecar: props.blobSidecar, source: props.source, seenTimestampSec: props.seenTimestampSec, peerIdStr: props.peerIdStr, }); return blockInput; } getLogMeta() { return { blockRoot: prettyBytes(this.blockRootHex), slot: this.slot, timeCreatedSec: this.timeCreatedSec, expectedBlobs: this.state.hasBlock ? this.state.block.message.body.blobKzgCommitments.length : "unknown", receivedBlobs: this.blobsCache.size, }; } addBlock({ blockRootHex, block, source }) { if (this.state.hasBlock) { throw new BlockInputError({ code: BlockInputErrorCode.INVALID_CONSTRUCTION, blockRoot: this.blockRootHex, }, "Cannot addBlock to BlockInputBlobs after it already has a block"); } // this check suffices for checking slot, parentRoot, and forkName if (blockRootHex !== this.blockRootHex) { throw new BlockInputError({ code: BlockInputErrorCode.MISMATCHED_ROOT_HEX, blockInputRoot: this.blockRootHex, mismatchedRoot: blockRootHex, source: source.source, peerId: `${source.peerIdStr}`, }, "addBlock blockRootHex does not match BlockInput.blockRootHex"); } for (const { blobSidecar } of this.blobsCache.values()) { if (!blockAndBlobArePaired(block, blobSidecar)) { this.blobsCache.delete(blobSidecar.index); // TODO: (@matthewkeil) spec says to ignore invalid blobs but should we downscore the peer maybe? // this.logger?.error(`Removing blobIndex=${blobSidecar.index} from BlockInput`, {}, err); } } const hasAllData = this.blobsCache.size === block.message.body.blobKzgCommitments.length; this.state = { ...this.state, hasBlock: true, hasAllData, block, versionedHashes: block.message.body.blobKzgCommitments.map(kzgCommitmentToVersionedHash), source, timeCompleteSec: hasAllData ? source.seenTimestampSec : undefined, }; this.blockPromise.resolve(block); if (hasAllData) { this.dataPromise.resolve(this.getBlobs()); } } hasBlob(blobIndex) { return this.blobsCache.has(blobIndex); } addBlob({ blockRootHex, blobSidecar, source, peerIdStr, seenTimestampSec }) { if (this.state.hasAllData) { throw new BlockInputError({ code: BlockInputErrorCode.INVALID_CONSTRUCTION, blockRoot: this.blockRootHex, }, "Cannot addBlob to BlockInputBlobs after it already is complete"); } if (this.blobsCache.has(blobSidecar.index)) { throw new BlockInputError({ code: BlockInputErrorCode.INVALID_CONSTRUCTION, blockRoot: this.blockRootHex, }, "Cannot addBlob to BlockInputBlobs with duplicate blobIndex"); } // this check suffices for checking slot, parentRoot, and forkName if (blockRootHex !== this.blockRootHex) { throw new BlockInputError({ code: BlockInputErrorCode.MISMATCHED_ROOT_HEX, blockInputRoot: this.blockRootHex, mismatchedRoot: blockRootHex, source: source, peerId: `${peerIdStr}`, }, "Blob BeaconBlockHeader blockRootHex does not match BlockInput.blockRootHex"); } if (this.state.hasBlock) { assertBlockAndBlobArePaired(this.blockRootHex, this.state.block, blobSidecar); } this.blobsCache.set(blobSidecar.index, { blobSidecar, source, seenTimestampSec, peerIdStr }); if (this.state.hasBlock && this.blobsCache.size === this.state.block.message.body.blobKzgCommitments.length) { this.state = { ...this.state, hasAllData: true, timeCompleteSec: seenTimestampSec, }; this.dataPromise.resolve([...this.blobsCache.values()].map(({ blobSidecar }) => blobSidecar)); } } getVersionedHashes() { if (!this.state.hasBlock) { throw new BlockInputError({ code: BlockInputErrorCode.INCOMPLETE_DATA, ...this.getLogMeta(), }, "Cannot get versioned hashes. Block is unknown"); } return this.state.versionedHashes; } getMissingBlobMeta() { if (!this.state.hasBlock) { throw new BlockInputError({ code: BlockInputErrorCode.INCOMPLETE_DATA, ...this.getLogMeta(), }, "Cannot get missing blobs. Block is unknown"); } if (this.state.hasAllData) { return []; } const blobMeta = []; const versionedHashes = this.state.versionedHashes; for (let index = 0; index < versionedHashes.length; index++) { if (!this.blobsCache.has(index)) { blobMeta.push({ index, blockRoot: fromHex(this.blockRootHex), versionHash: versionedHashes[index], }); } } return blobMeta; } getAllBlobsWithSource() { if (!this.state.hasAllData) { throw new BlockInputError({ code: BlockInputErrorCode.INCOMPLETE_DATA, ...this.getLogMeta(), }, "Cannot get all blobs. DA status is not complete"); } return [...this.blobsCache.values()]; } getBlobs() { return this.getAllBlobsWithSource().map(({ blobSidecar }) => blobSidecar); } } function blockAndBlobArePaired(block, blobSidecar) { const blockCommitment = block.message.body.blobKzgCommitments[blobSidecar.index]; if (!blockCommitment || !blobSidecar.kzgCommitment) { return false; } return Buffer.compare(blockCommitment, blobSidecar.kzgCommitment) === 0; } function assertBlockAndBlobArePaired(blockRootHex, block, blobSidecar) { if (!blockAndBlobArePaired(block, blobSidecar)) { // TODO: (@matthewkeil) should this eject the bad blob instead? No way to tell if the blob or the block // has the invalid commitment. Guessing it would be the blob though because we match via block // hashTreeRoot and we do not take a hashTreeRoot of the BlobSidecar throw new BlockInputError({ code: BlockInputErrorCode.MISMATCHED_KZG_COMMITMENT, blockRoot: blockRootHex, slot: block.message.slot, sidecarIndex: blobSidecar.index, }, "BlobSidecar commitment does not match block commitment"); } } /** * With columns, BlockInput has several states: * - The block is seen and all required sampled columns are seen * - The block is seen and all required sampled columns are not yet seen * - The block is not yet seen and all required sampled columns are seen * - The block is not yet seen and all required sampled columns are not yet seen */ export class BlockInputColumns extends AbstractBlockInput { constructor(init, state, sampledColumns, custodyColumns) { super(init); this.type = DAType.Columns; this.columnsCache = new Map(); this.state = state; this.sampledColumns = sampledColumns; this.custodyColumns = custodyColumns; } static createFromBlock(props) { const hasAllData = props.daOutOfRange || props.block.message.body.blobKzgCommitments.length === 0 || props.sampledColumns.length === 0; const state = { hasBlock: true, hasAllData, versionedHashes: props.block.message.body.blobKzgCommitments.map(kzgCommitmentToVersionedHash), block: props.block, source: props.source, timeCreated: props.source.seenTimestampSec, timeCompleteSec: hasAllData ? props.source.seenTimestampSec : undefined, }; const init = { daOutOfRange: props.daOutOfRange, timeCreated: props.source.seenTimestampSec, forkName: props.forkName, blockRootHex: props.blockRootHex, parentRootHex: toRootHex(props.block.message.parentRoot), slot: props.block.message.slot, }; const blockInput = new BlockInputColumns(init, state, props.sampledColumns, props.custodyColumns); blockInput.blockPromise.resolve(props.block); if (hasAllData) { blockInput.dataPromise.resolve([]); } return blockInput; } static createFromColumn(props) { const hasAllData = props.sampledColumns.length === 0; const state = { hasBlock: false, hasAllData, versionedHashes: props.columnSidecar.kzgCommitments.map(kzgCommitmentToVersionedHash), }; const init = { daOutOfRange: false, timeCreated: props.seenTimestampSec, forkName: props.forkName, blockRootHex: props.blockRootHex, parentRootHex: toRootHex(props.columnSidecar.signedBlockHeader.message.parentRoot), slot: props.columnSidecar.signedBlockHeader.message.slot, }; const blockInput = new BlockInputColumns(init, state, props.sampledColumns, props.custodyColumns); if (hasAllData) { blockInput.dataPromise.resolve([]); } return blockInput; } getLogMeta() { return { blockRoot: prettyBytes(this.blockRootHex), slot: this.slot, timeCreatedSec: this.timeCreatedSec, expectedColumns: this.state.hasBlock && this.state.block.message.body.blobKzgCommitments.length === 0 ? 0 : this.sampledColumns.length, receivedColumns: this.getSampledColumns().length, }; } addBlock(props) { if (this.state.hasBlock) { throw new BlockInputError({ code: BlockInputErrorCode.INVALID_CONSTRUCTION, blockRoot: this.blockRootHex, }, "Cannot addBlock to BlockInputColumns after it already has a block"); } if (props.blockRootHex !== this.blockRootHex) { throw new BlockInputError({ code: BlockInputErrorCode.MISMATCHED_ROOT_HEX, blockInputRoot: this.blockRootHex, mismatchedRoot: props.blockRootHex, source: props.source.source, peerId: `${props.source.peerIdStr}`, }, "addBlock blockRootHex does not match BlockInput.blockRootHex"); } for (const { columnSidecar } of this.columnsCache.values()) { if (!blockAndColumnArePaired(props.block, columnSidecar)) { this.columnsCache.delete(columnSidecar.index); // this.logger?.error(`Removing columnIndex=${columnSidecar.index} from BlockInput`, {}, err); } } const hasAllData = props.block.message.body.blobKzgCommitments.length === 0 || this.state.hasAllData; this.state = { ...this.state, hasBlock: true, hasAllData, block: props.block, source: props.source, timeCompleteSec: hasAllData ? props.source.seenTimestampSec : undefined, }; this.blockPromise.resolve(props.block); } addColumn({ blockRootHex, columnSidecar, source, seenTimestampSec, peerIdStr }) { if (blockRootHex !== this.blockRootHex) { throw new BlockInputError({ code: BlockInputErrorCode.MISMATCHED_ROOT_HEX, blockInputRoot: this.blockRootHex, mismatchedRoot: blockRootHex, source: source, peerId: `${peerIdStr}`, }, "Column BeaconBlockHeader blockRootHex does not match BlockInput.blockRootHex"); } if (this.state.hasBlock) { assertBlockAndColumnArePaired(this.blockRootHex, this.state.block, columnSidecar); } this.columnsCache.set(columnSidecar.index, { columnSidecar, source, seenTimestampSec, peerIdStr }); const sampledColumns = this.getSampledColumns(); const hasAllData = this.state.hasAllData || sampledColumns.length === this.sampledColumns.length; this.state = { ...this.state, hasAllData: hasAllData || this.state.hasAllData, timeCompleteSec: hasAllData ? seenTimestampSec : undefined, }; if (hasAllData && sampledColumns !== null) { this.dataPromise.resolve(sampledColumns); } } hasColumn(columnIndex) { return this.columnsCache.has(columnIndex); } getVersionedHashes() { return this.state.versionedHashes; } getCustodyColumns() { const columns = []; for (const index of this.custodyColumns) { const column = this.columnsCache.get(index); if (column) { columns.push(column.columnSidecar); } } return columns; } getSampledColumns() { const columns = []; for (const index of this.sampledColumns) { const column = this.columnsCache.get(index); if (column) { columns.push(column.columnSidecar); } } return columns; } getAllColumnsWithSource() { return [...this.columnsCache.values()]; } getAllColumns() { return this.getAllColumnsWithSource().map(({ columnSidecar }) => columnSidecar); } getMissingSampledColumnMeta() { if (this.state.hasAllData) { return []; } const needed = []; const blockRoot = fromHex(this.blockRootHex); for (const index of this.sampledColumns) { if (!this.columnsCache.has(index)) { needed.push({ index, blockRoot }); } } return needed; } } function blockAndColumnArePaired(block, columnSidecar) { return (block.message.body.blobKzgCommitments.length === columnSidecar.kzgCommitments.length && block.message.body.blobKzgCommitments.every((commitment, index) => Buffer.compare(commitment, columnSidecar.kzgCommitments[index]))); } function assertBlockAndColumnArePaired(blockRootHex, block, columnSidecar) { if (!blockAndColumnArePaired(block, columnSidecar)) { throw new BlockInputError({ code: BlockInputErrorCode.MISMATCHED_KZG_COMMITMENT, blockRoot: blockRootHex, slot: block.message.slot, sidecarIndex: columnSidecar.index, }, "DataColumnsSidecar kzgCommitment does not match block kzgCommitment"); } } //# sourceMappingURL=blockInput.js.map