@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
199 lines • 9.64 kB
JavaScript
import { routes } from "@lodestar/api";
import { signedBlockToSignedHeader } from "@lodestar/state-transition";
import { isGloasDataColumnSidecar } from "@lodestar/types";
import { fromHex, toHex } from "@lodestar/utils";
import { isBlockInputBlobs, isBlockInputColumns } from "../chain/blocks/blockInput/blockInput.js";
import { BlockInputSource } from "../chain/blocks/blockInput/types.js";
import { PayloadEnvelopeInput, PayloadEnvelopeInputSource } from "../chain/blocks/payloadEnvelopeInput/index.js";
import { ChainEvent } from "../chain/emitter.js";
import { computePreFuluKzgCommitmentsInclusionProof } from "./blobs.js";
import { getCellsAndProofs, getDataColumnSidecarsFromBlock, getDataColumnSidecarsFromColumnSidecar, getGloasDataColumnSidecars, } from "./dataColumns.js";
export { DataColumnEngineResult };
var DataColumnEngineResult;
(function (DataColumnEngineResult) {
DataColumnEngineResult["PreFulu"] = "pre_fulu";
// the recover is not attempted because it has full data columns
DataColumnEngineResult["NotAttemptedFull"] = "not_attempted_full";
// block has no blob so no need to call EL
DataColumnEngineResult["NotAttemptedNoBlobs"] = "not_attempted_no_blobs";
// EL call returned null, meaning it could not find the blobs
DataColumnEngineResult["NullResponse"] = "null_response";
// the recover is a success and it helps resolve availability
DataColumnEngineResult["SuccessResolved"] = "success_resolved";
// the recover is a success but it's late, availability is already resolved by either gossip or getBlobsV2
DataColumnEngineResult["SuccessLate"] = "success_late";
DataColumnEngineResult["Failed"] = "failed";
})(DataColumnEngineResult || (DataColumnEngineResult = {}));
export async function getBlobSidecarsFromExecution(config, executionEngine, metrics, emitter, blockInput) {
if (!isBlockInputBlobs(blockInput)) {
return;
}
if (blockInput.hasAllData()) {
return;
}
const forkName = blockInput.forkName;
const blobMeta = blockInput.getMissingBlobMeta();
metrics?.blobs.getBlobsV1Requests.inc();
metrics?.blobs.getBlobsV1RequestedBlobCount.inc(blobMeta.length);
const enginedResponse = await executionEngine
.getBlobs(forkName, blobMeta.map(({ versionedHash }) => versionedHash))
.catch((_e) => {
// TODO(fulu): this should only count as a single error? need to update the promql to reflect this
metrics?.blobs.getBlobsV1Error.inc(blobMeta.length);
return null;
});
if (enginedResponse === null) {
return;
}
const block = blockInput.getBlock();
const blobSidecars = [];
// response.length should always match blobMeta.length and they should be in the same order
for (let i = 0; i < blobMeta.length; i++) {
const blobAndProof = enginedResponse[i];
if (!blobAndProof) {
metrics?.blobs.getBlobsV1Miss.inc();
}
else {
metrics?.blobs.getBlobsV1Hit.inc();
if (blockInput.hasBlob(blobMeta[i].index)) {
// blob arrived and was cached while waiting for API response
metrics?.blobs.getBlobsV1HitButArrivedWhileWaiting.inc();
continue;
}
metrics?.blobs.getBlobsV1HitUseful.inc();
const { blob, proof } = blobAndProof;
const index = blobMeta[i].index;
const kzgCommitment = block.message.body.blobKzgCommitments[index];
const blobSidecar = {
index,
blob,
kzgProof: proof,
kzgCommitment,
// TODO(fulu): refactor this to only calculate the root inside these following two functions once
kzgCommitmentInclusionProof: computePreFuluKzgCommitmentsInclusionProof(forkName, block.message.body, index),
signedBlockHeader: signedBlockToSignedHeader(config, block),
};
blockInput.addBlob({
blobSidecar,
blockRootHex: blockInput.blockRootHex,
seenTimestampSec: Date.now() / 1000,
source: BlockInputSource.engine,
});
if (emitter.listenerCount(routes.events.EventType.blobSidecar)) {
emitter.emit(routes.events.EventType.blobSidecar, {
blockRoot: blockInput.blockRootHex,
slot: blockInput.slot,
index,
kzgCommitment: toHex(kzgCommitment),
versionedHash: toHex(blobMeta[i].versionedHash),
});
}
blobSidecars.push(blobSidecar);
}
}
emitter.emit(ChainEvent.publishBlobSidecars, blobSidecars);
metrics?.gossipBlob.publishedFromEngine.inc(blobSidecars.length);
}
/**
* Call getBlobsV2 from execution engine once per slot to fetch blobs and compute data columns.
*
* Post fulu, whenever we see either beacon_block or data_column_sidecar gossip message and data isn't complete.
* Post gloas, immediately when beacon block is successfully imported and PayloadEnvelopeInput is created.
*/
export async function getDataColumnSidecarsFromExecution(config, executionEngine, emitter, input, metrics, blobAndProofBuffers) {
const isPayloadInput = input instanceof PayloadEnvelopeInput;
// Pre gloas, ensure it's a column block input
if (!isPayloadInput && !isBlockInputColumns(input)) {
return DataColumnEngineResult.PreFulu;
}
// If already have all columns, exit
if (input.hasAllData()) {
return DataColumnEngineResult.NotAttemptedFull;
}
const versionedHashes = input.getVersionedHashes();
// If there are no blobs in this block, exit
if (versionedHashes.length === 0) {
return DataColumnEngineResult.NotAttemptedNoBlobs;
}
// Get blobs from execution engine
metrics?.peerDas.getBlobsV2Requests.inc();
const timer = metrics?.peerDas.getBlobsV2RequestDuration.startTimer();
const blobs = await executionEngine.getBlobs(input.forkName, versionedHashes, blobAndProofBuffers);
timer?.();
// Execution engine was unable to find one or more blobs
if (blobs === null) {
return DataColumnEngineResult.NullResponse;
}
metrics?.peerDas.getBlobsV2Responses.inc();
// Return if we received all data columns while waiting for getBlobs
if (input.hasAllData()) {
return DataColumnEngineResult.SuccessLate;
}
let dataColumnSidecars;
const compTimer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();
try {
const cellsAndProofs = await getCellsAndProofs(blobs);
if (isPayloadInput) {
dataColumnSidecars = getGloasDataColumnSidecars(input.slot, fromHex(input.blockRootHex), cellsAndProofs);
}
else if (input.hasBlock()) {
dataColumnSidecars = getDataColumnSidecarsFromBlock(config, input.getBlock(), cellsAndProofs);
}
else {
const firstSidecar = input.getAllColumns()[0];
dataColumnSidecars = getDataColumnSidecarsFromColumnSidecar(firstSidecar, cellsAndProofs);
}
}
finally {
compTimer?.();
}
// Publish columns if and only if subscribed to them
const previouslyMissingColumns = input.getMissingSampledColumnMeta().missing;
const sampledColumns = previouslyMissingColumns.map((columnIndex) => dataColumnSidecars[columnIndex]);
// for columns that we already seen, it will be ignored through `ignoreDuplicatePublishError` gossip option
emitter.emit(ChainEvent.publishDataColumns, sampledColumns);
// TODO: Can we record dataColumns.sentPeersPerSubnet metric here somehow
// add all sampled columns to the input, even if we didn't sample them
const seenTimestampSec = Date.now() / 1000;
let alreadyAddedColumnsCount = 0;
for (const columnSidecar of sampledColumns) {
if (input.hasColumn(columnSidecar.index)) {
// columns may have been added while waiting
alreadyAddedColumnsCount++;
continue;
}
if (isPayloadInput) {
if (!isGloasDataColumnSidecar(columnSidecar)) {
throw new Error(`Expected gloas DataColumnSidecar for block ${input.blockRootHex}`);
}
input.addColumn({
columnSidecar,
source: PayloadEnvelopeInputSource.engine,
seenTimestampSec,
});
}
else {
if (isGloasDataColumnSidecar(columnSidecar)) {
throw new Error(`Expected fulu DataColumnSidecar for block ${input.blockRootHex}`);
}
input.addColumn({
columnSidecar,
blockRootHex: input.blockRootHex,
source: BlockInputSource.engine,
seenTimestampSec,
});
}
if (emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) {
emitter.emit(routes.events.EventType.dataColumnSidecar, {
blockRoot: input.blockRootHex,
slot: input.slot,
index: columnSidecar.index,
kzgCommitments: !isGloasDataColumnSidecar(columnSidecar) ? columnSidecar.kzgCommitments.map(toHex) : undefined,
});
}
}
metrics?.dataColumns.alreadyAdded.inc(alreadyAddedColumnsCount);
metrics?.dataColumns.bySource.inc({ source: BlockInputSource.engine }, previouslyMissingColumns.length - alreadyAddedColumnsCount);
return DataColumnEngineResult.SuccessResolved;
}
//# sourceMappingURL=execution.js.map