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