UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

165 lines 9.44 kB
import { ExecutionStatus } from "@lodestar/fork-choice"; import { ForkName } from "@lodestar/params"; import { DataAvailabilityStatus, computeEpochAtSlot, isStateValidatorsNodesPopulated, } from "@lodestar/state-transition"; import { toRootHex } from "@lodestar/utils"; import { BlockError, BlockErrorCode } from "../errors/index.js"; import { RegenCaller } from "../regen/index.js"; import { BlockInputType } from "./types.js"; import { DENEB_BLOWFISH_BANNER } from "./utils/blowfishBanner.js"; import { ELECTRA_GIRAFFE_BANNER } from "./utils/giraffeBanner.js"; import { CAPELLA_OWL_BANNER } from "./utils/ownBanner.js"; import { POS_PANDA_MERGE_TRANSITION_BANNER } from "./utils/pandaMergeTransitionBanner.js"; import { verifyBlocksDataAvailability } from "./verifyBlocksDataAvailability.js"; import { verifyBlocksExecutionPayload } from "./verifyBlocksExecutionPayloads.js"; import { verifyBlocksSignatures } from "./verifyBlocksSignatures.js"; import { verifyBlocksStateTransitionOnly } from "./verifyBlocksStateTransitionOnly.js"; import { writeBlockInputToDb } from "./writeBlockInputToDb.js"; /** * Verifies 1 or more blocks are fully valid; from a linear sequence of blocks. * * To relieve the main thread signatures are verified separately in workers with chain.bls worker pool. * In parallel it: * - Run full state transition in sequence * - Verify all block's signatures in parallel * - Submit execution payloads to EL in sequence * * If there's an error during one of the steps, the rest are aborted with an AbortController. */ export async function verifyBlocksInEpoch(parentBlock, blocksInput, opts) { const blocks = blocksInput.map(({ block }) => block); const lastBlock = blocks.at(-1); if (!lastBlock) { throw Error("Empty partiallyVerifiedBlocks"); } const block0 = blocks[0]; const block0Epoch = computeEpochAtSlot(block0.message.slot); // Ensure all blocks are in the same epoch for (let i = 1; i < blocks.length; i++) { const blockSlot = blocks[i].message.slot; if (block0Epoch !== computeEpochAtSlot(blockSlot)) { throw Error(`Block ${i} slot ${blockSlot} not in same epoch ${block0Epoch}`); } } // TODO: Skip in process chain segment // Retrieve preState from cache (regen) const preState0 = await this.regen // transfer cache to process faster, postState will be in block state cache .getPreState(block0.message, { dontTransferCache: false }, RegenCaller.processBlocksInEpoch) .catch((e) => { throw new BlockError(block0, { code: BlockErrorCode.PRESTATE_MISSING, error: e }); }); if (!isStateValidatorsNodesPopulated(preState0)) { this.logger.verbose("verifyBlocksInEpoch preState0 SSZ cache stats", { slot: preState0.slot, cache: isStateValidatorsNodesPopulated(preState0), clonedCount: preState0.clonedCount, clonedCountWithTransferCache: preState0.clonedCountWithTransferCache, createdWithTransferCache: preState0.createdWithTransferCache, }); } // Ensure the state is in the same epoch as block0 if (block0Epoch !== computeEpochAtSlot(preState0.slot)) { throw Error(`preState at slot ${preState0.slot} must be dialed to block epoch ${block0Epoch}`); } const abortController = new AbortController(); try { // batch all I/O operations to reduce overhead const [segmentExecStatus, { dataAvailabilityStatuses, availableTime, availableBlockInputs }, { postStates, proposerBalanceDeltas, verifyStateTime }, { verifySignaturesTime },] = await Promise.all([ // Execution payloads opts.skipVerifyExecutionPayload !== true ? verifyBlocksExecutionPayload(this, parentBlock, blocks, preState0, abortController.signal, opts) : Promise.resolve({ execAborted: null, executionStatuses: blocks.map((_blk) => ExecutionStatus.Syncing), mergeBlockFound: null, }), // data availability for the blobs verifyBlocksDataAvailability(this, blocksInput, abortController.signal, opts), // Run state transition only // TODO: Ensure it yields to allow flushing to workers and engine API verifyBlocksStateTransitionOnly(preState0, blocksInput, // hack availability for state transition eval as availability is separately determined blocks.map(() => DataAvailabilityStatus.Available), this.logger, this.metrics, this.validatorMonitor, abortController.signal, opts), // All signatures at once opts.skipVerifyBlockSignatures !== true ? verifyBlocksSignatures(this.bls, this.logger, this.metrics, preState0, blocks, opts) : Promise.resolve({ verifySignaturesTime: Date.now() }), // ideally we want to only persist blocks after verifying them however the reality is there are // rarely invalid blocks we'll batch all I/O operation here to reduce the overhead if there's // an error, we'll remove blocks not in forkchoice opts.verifyOnly !== true && opts.eagerPersistBlock ? writeBlockInputToDb.call(this, blocksInput) : Promise.resolve(), ]); if (opts.verifyOnly !== true) { if (segmentExecStatus.execAborted === null && segmentExecStatus.mergeBlockFound !== null) { // merge block found and is fully valid = state transition + signatures + execution payload. // TODO: Will this banner be logged during syncing? logOnPowBlock(this.logger, this.config, segmentExecStatus.mergeBlockFound); } const fromFork = this.config.getForkName(parentBlock.slot); const toFork = this.config.getForkName(lastBlock.message.slot); // If transition through toFork, note won't happen if ${toFork}_EPOCH = 0, will log double on re-org if (toFork !== fromFork) { switch (toFork) { case ForkName.capella: this.logger.info(CAPELLA_OWL_BANNER); this.logger.info("Activating withdrawals", { epoch: this.config.CAPELLA_FORK_EPOCH }); break; case ForkName.deneb: this.logger.info(DENEB_BLOWFISH_BANNER); this.logger.info("Activating blobs", { epoch: this.config.DENEB_FORK_EPOCH }); break; case ForkName.electra: this.logger.info(ELECTRA_GIRAFFE_BANNER); this.logger.info("Activating maxEB", { epoch: this.config.ELECTRA_FORK_EPOCH }); break; default: } } } if (segmentExecStatus.execAborted === null) { const { executionStatuses, executionTime } = segmentExecStatus; if (blocksInput.length === 1 && // gossip blocks have seenTimestampSec opts.seenTimestampSec !== undefined && blocksInput[0].type !== BlockInputType.preData && executionStatuses[0] === ExecutionStatus.Valid) { // Find the max time when the block was actually verified const fullyVerifiedTime = Math.max(executionTime, verifyStateTime, verifySignaturesTime); const recvTofullyVerifedTime = fullyVerifiedTime / 1000 - opts.seenTimestampSec; this.metrics?.gossipBlock.receivedToFullyVerifiedTime.observe(recvTofullyVerifedTime); const verifiedToBlobsAvailabiltyTime = Math.max(availableTime - fullyVerifiedTime, 0) / 1000; const numBlobs = blocksInput[0].block.message.body.blobKzgCommitments.length; this.metrics?.gossipBlock.verifiedToBlobsAvailabiltyTime.observe({ numBlobs }, verifiedToBlobsAvailabiltyTime); this.logger.verbose("Verified blockInput fully with blobs availability", { slot: blocksInput[0].block.message.slot, recvTofullyVerifedTime, verifiedToBlobsAvailabiltyTime, type: blocksInput[0].type, numBlobs, }); } } else { this.logger.verbose("Block verification aborted due to execution payload", {}, segmentExecStatus.execAborted.execError); } return { postStates, dataAvailabilityStatuses, proposerBalanceDeltas, segmentExecStatus, availableBlockInputs }; } finally { abortController.abort(); } } function logOnPowBlock(logger, config, mergeBlock) { const mergeBlockHash = toRootHex(config.getForkTypes(mergeBlock.slot).BeaconBlock.hashTreeRoot(mergeBlock)); const mergeExecutionHash = toRootHex(mergeBlock.body.executionPayload.blockHash); const mergePowHash = toRootHex(mergeBlock.body.executionPayload.parentHash); logger.info(POS_PANDA_MERGE_TRANSITION_BANNER); logger.info("Execution transitioning from PoW to PoS!!!"); logger.info("Importing block referencing terminal PoW block", { blockHash: mergeBlockHash, executionHash: mergeExecutionHash, powHash: mergePowHash, }); } //# sourceMappingURL=verifyBlock.js.map