UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

210 lines 12.3 kB
import { ExecutionStatus } from "@lodestar/fork-choice"; import { ForkName, ForkSeq, isForkPostFulu } from "@lodestar/params"; import { DataAvailabilityStatus, computeEpochAtSlot } from "@lodestar/state-transition"; import { getBlobKzgCommitments } from "../../util/dataColumns.js"; import { BlockError, BlockErrorCode } from "../errors/index.js"; import { RegenCaller } from "../regen/index.js"; import { DAType } from "./blockInput/index.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 { FULU_ZEBRA_BANNER } from "./utils/zebraBanner.js"; import { verifyBlocksDataAvailability } from "./verifyBlocksDataAvailability.js"; import { verifyBlocksExecutionPayload } from "./verifyBlocksExecutionPayloads.js"; import { verifyBlocksSignatures } from "./verifyBlocksSignatures.js"; import { verifyBlocksStateTransitionOnly } from "./verifyBlocksStateTransitionOnly.js"; import { verifyPayloadsDataAvailability } from "./verifyPayloadsDataAvailability.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, blockInputs, payloadEnvelopes, opts) { const blocks = blockInputs.map((blockInput) => blockInput.getBlock()); 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}`); } } // All blocks are in the same epoch const fork = this.config.getForkSeq(block0.message.slot); // 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 }); }); // in forky condition, make sure to populate ShufflingCache with regened state // otherwise it may fail to get indexed attestations from shuffling cache later this.shufflingCache.processState(preState0); if (!preState0.isStateValidatorsNodesPopulated()) { this.logger.verbose("verifyBlocksInEpoch preState0 SSZ cache stats", { slot: preState0.slot, cache: preState0.isStateValidatorsNodesPopulated(), 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 { // Start execution payload verification first (async request to execution client) const verifyExecutionPayloadsPromise = opts.skipVerifyExecutionPayload !== true ? verifyBlocksExecutionPayload(this, parentBlock, blockInputs, preState0, abortController.signal, opts) : Promise.resolve({ execAborted: null, executionStatuses: blocks.map((_blk) => ExecutionStatus.Syncing), }); // Store indexed attestations for each block to avoid recomputing them during import const indexedAttestationsByBlock = []; for (const [i, block] of blocks.entries()) { indexedAttestationsByBlock[i] = block.message.body.attestations.map((attestation) => { const attEpoch = computeEpochAtSlot(attestation.data.slot); const decisionRoot = preState0.getShufflingDecisionRoot(attEpoch); return this.shufflingCache.getIndexedAttestation(attEpoch, decisionRoot, fork, attestation); }); } // Pick the data-availability source by fork: // - Pre-Gloas: blob/Fulu-column data lives in IBlockInput → verifyBlocksDataAvailability. // - Post-Gloas: verifyPayloadsDataAvailability (payload-level DA, keyed by slot). const daAvailabilityPromise = fork >= ForkSeq.gloas ? (async () => { // Validate DA for ALL payloads in the Map, not just those paired with blockInputs. // A checkpoint-sync batch may include a payload for a slot whose block was filtered // out of relevantBlocks (e.g., the anchor at the finalized slot); that payload still // needs DA validation so it can be imported in processBlocks. const payloadInputsForDa = payloadEnvelopes !== null ? Array.from(payloadEnvelopes.values()) : []; const { dataAvailabilityStatuses, availableTime } = await verifyPayloadsDataAvailability(payloadInputsForDa, abortController.signal); const payloadDAStatuses = new Map(); for (let i = 0; i < payloadInputsForDa.length; i++) { payloadDAStatuses.set(payloadInputsForDa[i].slot, dataAvailabilityStatuses[i]); } return { // post-gloas, DataAvailabilityStatus is NotRequired for forkChoice.onBlock() ProtoBlock blockDAStatuses: blockInputs.map(() => DataAvailabilityStatus.NotRequired), payloadDAStatuses, availableTime, }; })() : (async () => { const { dataAvailabilityStatuses, availableTime } = await verifyBlocksDataAvailability(blockInputs, abortController.signal); return { blockDAStatuses: dataAvailabilityStatuses, payloadDAStatuses: new Map(), availableTime, }; })(); // batch all I/O operations to reduce overhead const [segmentExecStatus, { blockDAStatuses, payloadDAStatuses, availableTime }, { postStates, proposerBalanceDeltas, verifyStateTime }, { verifySignaturesTime },] = await Promise.all([ verifyExecutionPayloadsPromise, // data availability (fork-specific; see daAvailabilityPromise above) daAvailabilityPromise, // Run state transition only // TODO: Ensure it yields to allow flushing to workers and engine API verifyBlocksStateTransitionOnly(preState0, blockInputs, // 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.config, this.bls, this.logger, this.metrics, preState0, blocks, indexedAttestationsByBlock, opts) : Promise.resolve({ verifySignaturesTime: Date.now() }), // TODO GLOAS: can verify payload signatures in batch too // maybe chain with the above verifyBlocksSignatures() ]); if (opts.verifyOnly !== true) { const fromForkBoundary = this.config.getForkBoundaryAtEpoch(computeEpochAtSlot(parentBlock.slot)); const toForkBoundary = this.config.getForkBoundaryAtEpoch(computeEpochAtSlot(lastBlock.message.slot)); // If transition through toFork, note won't happen if ${toFork}_EPOCH = 0, will log double on re-org if (toForkBoundary.fork !== fromForkBoundary.fork) { switch (toForkBoundary.fork) { 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; case ForkName.fulu: this.logger.info(FULU_ZEBRA_BANNER); this.logger.info("Activating peerDAS", { epoch: this.config.FULU_FORK_EPOCH }); break; default: } } if (isForkPostFulu(fromForkBoundary.fork)) { const fromBlobParameters = this.config.getBlobParameters(fromForkBoundary.epoch); const toBlobParameters = this.config.getBlobParameters(toForkBoundary.epoch); if (toBlobParameters.epoch !== fromBlobParameters.epoch) { const { epoch, maxBlobsPerBlock } = toBlobParameters; this.logger.info("Activating BPO fork", { epoch, maxBlobsPerBlock }); } } } if (segmentExecStatus.execAborted === null) { const { executionStatuses, executionTime } = segmentExecStatus; if (blockInputs.length === 1 && // gossip blocks have seenTimestampSec opts.seenTimestampSec !== undefined && // PreData (pre-deneb) and NoData (gloas) carry no blob data on the block — skip metric blockInputs[0].type !== DAType.PreData && blockInputs[0].type !== DAType.NoData && 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 block = blockInputs[0].getBlock(); const numBlobs = getBlobKzgCommitments(blockInputs[0].forkName, block).length; this.metrics?.gossipBlock.verifiedToBlobsAvailabiltyTime.observe({ numBlobs }, verifiedToBlobsAvailabiltyTime); this.logger.verbose("Verified blockInput fully with blobs availability", { slot: block.message.slot, recvTofullyVerifedTime, verifiedToBlobsAvailabiltyTime, type: blockInputs[0].type, numBlobs, }); } } else { this.logger.verbose("Block verification aborted due to execution payload", {}, segmentExecStatus.execAborted.execError); } return { postStates, blockDAStatuses, payloadDAStatuses, proposerBalanceDeltas, segmentExecStatus, indexedAttestationsByBlock, }; } finally { abortController.abort(); } } //# sourceMappingURL=verifyBlock.js.map