@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
107 lines • 5.97 kB
JavaScript
import { DataAvailabilityStatus, computeTimeAtSlot } from "@lodestar/state-transition";
import { ErrorAborted } from "@lodestar/utils";
import { BlockError, BlockErrorCode } from "../errors/index.js";
import { validateBlobSidecars } from "../validation/blobSidecar.js";
import { BlobSidecarValidation, BlockInputType, getBlockInput } from "./types.js";
// we can now wait for full 12 seconds because unavailable block sync will try pulling
// the blobs from the network anyway after 500ms of seeing the block
const BLOB_AVAILABILITY_TIMEOUT = 12_000;
/**
* Verifies some early cheap sanity checks on the block before running the full state transition.
*
* - Parent is known to the fork-choice
* - Check skipped slots limit
* - check_block_relevancy()
* - Block not in the future
* - Not genesis block
* - Block's slot is < Infinity
* - Not finalized slot
* - Not already known
*/
export async function verifyBlocksDataAvailability(chain, blocks, signal, opts) {
const lastBlock = blocks.at(-1);
if (!lastBlock) {
throw Error("Empty partiallyVerifiedBlocks");
}
const dataAvailabilityStatuses = [];
const seenTime = opts.seenTimestampSec !== undefined ? opts.seenTimestampSec * 1000 : Date.now();
const availableBlockInputs = [];
for (const blockInput of blocks) {
if (signal.aborted) {
throw new ErrorAborted("verifyBlocksDataAvailability");
}
// Validate status of only not yet finalized blocks, we don't need yet to propogate the status
// as it is not used upstream anywhere
const { dataAvailabilityStatus, availableBlockInput } = await maybeValidateBlobs(chain, blockInput, signal, opts);
dataAvailabilityStatuses.push(dataAvailabilityStatus);
availableBlockInputs.push(availableBlockInput);
}
const availableTime = lastBlock.type === BlockInputType.dataPromise ? Date.now() : seenTime;
if (blocks.length === 1 && opts.seenTimestampSec !== undefined && blocks[0].type !== BlockInputType.preData) {
const recvToAvailableTime = availableTime / 1000 - opts.seenTimestampSec;
const numBlobs = blocks[0].block.message.body.blobKzgCommitments.length;
chain.metrics?.gossipBlock.receivedToBlobsAvailabilityTime.observe({ numBlobs }, recvToAvailableTime);
chain.logger.verbose("Verified blobs availability", {
slot: blocks[0].block.message.slot,
recvToAvailableTime,
type: blocks[0].type,
});
}
return { dataAvailabilityStatuses, availableTime, availableBlockInputs };
}
async function maybeValidateBlobs(chain, blockInput, signal, opts) {
switch (blockInput.type) {
case BlockInputType.preData:
return { dataAvailabilityStatus: DataAvailabilityStatus.PreData, availableBlockInput: blockInput };
case BlockInputType.outOfRangeData:
return { dataAvailabilityStatus: DataAvailabilityStatus.OutOfRange, availableBlockInput: blockInput };
// biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
case BlockInputType.availableData:
if (opts.validBlobSidecars === BlobSidecarValidation.Full) {
return { dataAvailabilityStatus: DataAvailabilityStatus.Available, availableBlockInput: blockInput };
}
case BlockInputType.dataPromise: {
// run full validation
const { block } = blockInput;
const blockSlot = block.message.slot;
const blobsData = blockInput.type === BlockInputType.availableData
? blockInput.blockData
: await raceWithCutoff(chain, blockInput, blockInput.cachedData.availabilityPromise, signal);
const { blobs } = blobsData;
const { blobKzgCommitments } = block.message.body;
const beaconBlockRoot = chain.config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block.message);
// if the blob siddecars have been individually verified then we can skip kzg proof check
// but other checks to match blobs with block data still need to be performed
const skipProofsCheck = opts.validBlobSidecars === BlobSidecarValidation.Individual;
await validateBlobSidecars(blockSlot, beaconBlockRoot, blobKzgCommitments, blobs, { skipProofsCheck });
const availableBlockInput = getBlockInput.availableData(chain.config, blockInput.block, blockInput.source, blobsData);
return { dataAvailabilityStatus: DataAvailabilityStatus.Available, availableBlockInput: availableBlockInput };
}
}
}
/**
* Wait for blobs to become available with a cutoff time. If fails then throw DATA_UNAVAILABLE error
* which may try unknownblock/blobs fill (by root).
*/
async function raceWithCutoff(chain, blockInput, availabilityPromise, signal) {
const { block } = blockInput;
const blockSlot = block.message.slot;
const cutoffTime = computeTimeAtSlot(chain.config, blockSlot, chain.genesisTime) * 1000 + BLOB_AVAILABILITY_TIMEOUT - Date.now();
const cutoffTimeout = cutoffTime > 0
? new Promise((_resolve, reject) => {
setTimeout(() => reject(new Error("Timeout exceeded")), cutoffTime);
signal.addEventListener("abort", () => reject(signal.reason));
})
: Promise.reject(new Error("Cutoff time must be greater than 0"));
chain.logger.debug("Racing for blob availabilityPromise", { blockSlot, cutoffTime });
try {
await Promise.race([availabilityPromise, cutoffTimeout]);
}
catch (_e) {
// throw unavailable so that the unknownblock/blobs can be triggered to pull the block
throw new BlockError(block, { code: BlockErrorCode.DATA_UNAVAILABLE });
}
// we can only be here if availabilityPromise has resolved else an error will be thrown
return availabilityPromise;
}
//# sourceMappingURL=verifyBlocksDataAvailability.js.map