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