UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

932 lines (834 loc) • 34.5 kB
import {ChainForkConfig} from "@lodestar/config"; import {IForkChoice, ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; import { BUILDER_INDEX_SELF_BUILD, ForkName, ForkPostBellatrix, ForkPostCapella, ForkPostDeneb, ForkPostFulu, ForkPostGloas, ForkPreGloas, ForkSeq, isForkPostAltair, isForkPostBellatrix, isForkPostGloas, } from "@lodestar/params"; import { G2_POINT_AT_INFINITY, IBeaconStateView, type IBeaconStateViewBellatrix, computeTimeAtSlot, isStatePostBellatrix, isStatePostCapella, isStatePostGloas, } from "@lodestar/state-transition"; import { BLSPubkey, BLSSignature, BeaconBlock, BeaconBlockBody, BlindedBeaconBlock, BlindedBeaconBlockBody, BlobsBundle, Bytes32, ExecutionPayload, ExecutionPayloadHeader, Root, RootHex, SSEPayloadAttributes, Slot, ValidatorIndex, Wei, altair, capella, deneb, electra, fulu, gloas, ssz, } from "@lodestar/types"; import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {ZERO_HASH_HEX} from "../../constants/index.js"; import {numToQuantity} from "../../execution/engine/utils.js"; import { IExecutionBuilder, IExecutionEngine, PayloadAttributes, PayloadId, getExpectedGasLimit, } from "../../execution/index.js"; import {fromGraffitiBytes} from "../../util/graffiti.js"; import {kzg} from "../../util/kzg.js"; import type {BeaconChain} from "../chain.js"; import {CommonBlockBody} from "../interface.js"; import {validateBlobsAndKzgCommitments, validateCellsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js"; // Time to provide the EL to generate a payload from new payload id const PAYLOAD_GENERATION_TIME_MS = 500; export enum PayloadPreparationType { Fresh = "Fresh", Cached = "Cached", Reorged = "Reorged", Blinded = "Blinded", } /** * Block production steps tracked in metrics */ export enum BlockProductionStep { proposerSlashing = "proposerSlashing", attesterSlashings = "attesterSlashings", voluntaryExits = "voluntaryExits", blsToExecutionChanges = "blsToExecutionChanges", attestations = "attestations", syncAggregate = "syncAggregate", executionPayload = "executionPayload", } export type BlockAttributes = { randaoReveal: BLSSignature; graffiti: Bytes32; slot: Slot; parentBlock: ProtoBlock; feeRecipient?: string; }; export enum BlockType { Full = "Full", Blinded = "Blinded", } export type AssembledBodyType<T extends BlockType> = T extends BlockType.Full ? BeaconBlockBody : BlindedBeaconBlockBody; export type AssembledBlockType<T extends BlockType> = T extends BlockType.Full ? BeaconBlock : BlindedBeaconBlock; export type ProduceFullGloas = { type: BlockType.Full; fork: ForkPostGloas; executionPayload: ExecutionPayload<ForkPostGloas>; executionRequests: electra.ExecutionRequests; blobsBundle: BlobsBundle<ForkPostGloas>; cells: fulu.Cell[][]; parentBlockRoot: Root; }; export type ProduceFullFulu = { type: BlockType.Full; fork: ForkPostFulu; executionPayload: ExecutionPayload<ForkPostFulu>; blobsBundle: BlobsBundle<ForkPostFulu>; cells: fulu.Cell[][]; }; export type ProduceFullDeneb = { type: BlockType.Full; fork: ForkName.deneb | ForkName.electra; executionPayload: ExecutionPayload<ForkPostDeneb>; blobsBundle: BlobsBundle<ForkPostDeneb>; }; export type ProduceFullBellatrix = { type: BlockType.Full; fork: ForkName.bellatrix | ForkName.capella; executionPayload: ExecutionPayload<ForkPostBellatrix>; }; export type ProduceFullPhase0 = { type: BlockType.Full; fork: ForkName.phase0 | ForkName.altair; }; export type ProduceBlinded = { type: BlockType.Blinded; fork: ForkName; }; // The results of block production returned by `produceBlockBody` // The types are defined separately so typecasting can be used /** The result of local block production, everything that's not the block itself */ export type ProduceResult = | ProduceFullGloas | ProduceFullFulu | ProduceFullDeneb | ProduceFullBellatrix | ProduceFullPhase0 | ProduceBlinded; export async function produceBlockBody<T extends BlockType>( this: BeaconChain, blockType: T, currentState: IBeaconStateView, blockAttr: BlockAttributes & { proposerIndex: ValidatorIndex; proposerPubKey: BLSPubkey; commonBlockBodyPromise: Promise<CommonBlockBody>; } ): Promise<{ body: AssembledBodyType<T>; produceResult: ProduceResult; executionPayloadValue: Wei; shouldOverrideBuilder?: boolean; }> { const { slot: blockSlot, feeRecipient: requestedFeeRecipient, parentBlock, proposerIndex, proposerPubKey, commonBlockBodyPromise, } = blockAttr; let executionPayloadValue: Wei; let blockBody: AssembledBodyType<T>; const parentBlockRoot = fromHex(parentBlock.blockRoot); // even though shouldOverrideBuilder is relevant for the engine response, for simplicity of typing // we just return it undefined for the builder which anyway doesn't get consumed downstream let shouldOverrideBuilder: boolean | undefined; const fork = this.config.getForkName(blockSlot); const produceResult = { type: blockType, fork, } as ProduceResult; const logMeta: Record<string, string | number | bigint> = { fork, blockType, slot: blockSlot, }; this.logger.verbose("Producing beacon block body", logMeta); if (isForkPostGloas(fork)) { if (!isStatePostGloas(currentState)) { throw new Error("Expected Gloas state for Gloas block production"); } // TODO GLOAS: support non self-building here, the block type differentiation between // full and blinded no longer makes sense in gloas, it might be a good idea to move // this into a completely separate function and have pre/post gloas more separated const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex); const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer(); this.logger.verbose("Preparing execution payload from engine", { slot: blockSlot, parentBlockRoot: toRootHex(parentBlockRoot), feeRecipient, }); // Get execution payload from EL let parentBlockHash: Bytes32; let parentExecutionRequests: electra.ExecutionRequests; // Apply parent payload once here as it's reused by EL prep and voluntary exit filtering below let stateAfterParentPayload: IBeaconStateViewBellatrix = currentState; const isExtendingPayload = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot)); if (isExtendingPayload) { parentBlockHash = currentState.latestExecutionPayloadBid.blockHash; parentExecutionRequests = await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot); stateAfterParentPayload = currentState.withParentPayloadApplied(parentExecutionRequests); } else { parentBlockHash = currentState.latestExecutionPayloadBid.parentBlockHash; parentExecutionRequests = ssz.electra.ExecutionRequests.defaultValue(); } const prepareRes = await prepareExecutionPayload( this, this.logger, fork, parentBlockRoot, parentBlockHash, safeBlockHash, finalizedBlockHash ?? ZERO_HASH_HEX, stateAfterParentPayload, feeRecipient ); const {prepType, payloadId} = prepareRes; Object.assign(logMeta, {executionPayloadPrepType: prepType}); if (prepType !== PayloadPreparationType.Cached) { await sleep(PAYLOAD_GENERATION_TIME_MS); } this.logger.verbose("Fetching execution payload from engine", {slot: blockSlot, payloadId}); const payloadRes = await this.executionEngine.getPayload(fork, payloadId); endExecutionPayload?.({step: BlockProductionStep.executionPayload}); const {executionPayload, blobsBundle, executionRequests} = payloadRes; executionPayloadValue = payloadRes.executionPayloadValue; shouldOverrideBuilder = payloadRes.shouldOverrideBuilder; if (blobsBundle === undefined) { throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`); } if (executionRequests === undefined) { throw Error(`Missing executionRequests response from getPayload at fork=${fork}`); } const cells = blobsBundle.blobs.map((blob) => kzg.computeCells(blob)); if (this.opts.sanityCheckExecutionEngineBlobs) { await validateCellsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, cells); } // Create self-build execution payload bid const bid: gloas.ExecutionPayloadBid = { parentBlockHash, parentBlockRoot, blockHash: executionPayload.blockHash, prevRandao: currentState.getRandaoMix(currentState.epoch), feeRecipient: executionPayload.feeRecipient, gasLimit: BigInt(executionPayload.gasLimit), builderIndex: BUILDER_INDEX_SELF_BUILD, slot: blockSlot, value: 0, executionPayment: 0, blobKzgCommitments: blobsBundle.commitments, executionRequestsRoot: ssz.electra.ExecutionRequests.hashTreeRoot(executionRequests), }; const signedBid: gloas.SignedExecutionPayloadBid = { message: bid, signature: G2_POINT_AT_INFINITY, }; const commonBlockBody = await commonBlockBodyPromise; const gloasBody = Object.assign({}, commonBlockBody) as gloas.BeaconBlockBody; gloasBody.signedExecutionPayloadBid = signedBid; gloasBody.payloadAttestations = this.payloadAttestationPool.getPayloadAttestationsForBlock( parentBlock.blockRoot, blockSlot - 1 ); gloasBody.parentExecutionRequests = parentExecutionRequests; // Drop voluntary exits that parent_execution_requests have invalidated (e.g. a withdrawal // request initiating an exit on the same validator). Op pool selected against the unapplied // state, so re-validate against the post-apply state to avoid producing an invalid block. if (isExtendingPayload && commonBlockBody.voluntaryExits.length > 0) { gloasBody.voluntaryExits = commonBlockBody.voluntaryExits.filter((signedVoluntaryExit) => stateAfterParentPayload.isValidVoluntaryExit(signedVoluntaryExit, false) ); } blockBody = gloasBody as AssembledBodyType<T>; // Store execution payload data required to construct execution payload envelope later const gloasResult = produceResult as ProduceFullGloas; gloasResult.executionPayload = executionPayload as ExecutionPayload<ForkPostGloas>; gloasResult.executionRequests = executionRequests; gloasResult.blobsBundle = blobsBundle; gloasResult.cells = cells; gloasResult.parentBlockRoot = fromHex(parentBlock.blockRoot); const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime); this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime); this.logger.verbose("Produced block with self-build bid", { slot: blockSlot, executionPayloadValue, prepType, payloadId, fetchedTime, executionBlockHash: toRootHex(executionPayload.blockHash), blobs: blobsBundle.commitments.length, }); Object.assign(logMeta, { transactions: executionPayload.transactions.length, blobs: blobsBundle.commitments.length, shouldOverrideBuilder, }); } else if (isForkPostBellatrix(fork)) { if (!isStatePostBellatrix(currentState)) { throw new Error("Expected Bellatrix state for execution block production"); } const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex); const feeRecipientType = requestedFeeRecipient ? "requested" : this.beaconProposerCache.get(proposerIndex) ? "cached" : "default"; Object.assign(logMeta, {feeRecipientType, feeRecipient}); if (blockType === BlockType.Blinded) { if (!this.executionBuilder) throw Error("External builder not configured"); const executionBuilder = this.executionBuilder; const builderPromise = (async () => { const endExecutionPayloadHeader = this.metrics?.builderBlockProductionTimeSteps.startTimer(); // This path will not be used in the production, but is here just for merge mock // tests because merge-mock requires an fcU to be issued prior to fetch payload // header. if (executionBuilder.issueLocalFcUWithFeeRecipient !== undefined) { await prepareExecutionPayload( this, this.logger, fork, parentBlockRoot, currentState.latestExecutionPayloadHeader.blockHash, safeBlockHash, finalizedBlockHash ?? ZERO_HASH_HEX, currentState, executionBuilder.issueLocalFcUWithFeeRecipient ); } // For MeV boost integration, this is where the execution header will be // fetched from the payload id and a blinded block will be produced instead of // fullblock for the validator to sign this.logger.verbose("Fetching execution payload header from builder", { slot: blockSlot, proposerPubKey: toHex(proposerPubKey), }); const headerRes = await prepareExecutionPayloadHeader(this, fork, currentState, proposerPubKey); endExecutionPayloadHeader?.({ step: BlockProductionStep.executionPayload, }); return headerRes; })(); const [builderRes, commonBlockBody] = await Promise.all([builderPromise, commonBlockBodyPromise]); blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<BlockType.Blinded>; (blockBody as BlindedBeaconBlockBody).executionPayloadHeader = builderRes.header; executionPayloadValue = builderRes.executionPayloadValue; const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime); const prepType = PayloadPreparationType.Blinded; this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime); this.logger.verbose("Fetched execution payload header from builder", { slot: blockSlot, executionPayloadValue, prepType, fetchedTime, }); const targetGasLimit = executionBuilder.getValidatorRegistration(proposerPubKey)?.gasLimit; if (!targetGasLimit) { // This should only happen if cache was cleared due to restart of beacon node this.logger.warn("Failed to get validator registration, could not check header gas limit", { slot: blockSlot, proposerIndex, proposerPubKey: toPubkeyHex(proposerPubKey), }); } else { const headerGasLimit = builderRes.header.gasLimit; const parentGasLimit = currentState.latestExecutionPayloadHeader.gasLimit; const expectedGasLimit = getExpectedGasLimit(parentGasLimit, targetGasLimit); const lowerBound = Math.min(parentGasLimit, expectedGasLimit); const upperBound = Math.max(parentGasLimit, expectedGasLimit); if (headerGasLimit < lowerBound || headerGasLimit > upperBound) { throw Error( `Header gas limit ${headerGasLimit} is outside of acceptable range [${lowerBound}, ${upperBound}]` ); } if (headerGasLimit !== expectedGasLimit) { this.logger.warn("Header gas limit does not match expected value", { slot: blockSlot, headerGasLimit, expectedGasLimit, parentGasLimit, targetGasLimit, }); } } if (ForkSeq[fork] >= ForkSeq.deneb) { const {blobKzgCommitments} = builderRes; if (blobKzgCommitments === undefined) { throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`); } (blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments; Object.assign(logMeta, {blobs: blobKzgCommitments.length}); } if (ForkSeq[fork] >= ForkSeq.electra) { const {executionRequests} = builderRes; if (executionRequests === undefined) { throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`); } (blockBody as electra.BlindedBeaconBlockBody).executionRequests = executionRequests; } } // blockType === BlockType.Full else { // enginePromise only supports pre-gloas const enginePromise = (async () => { const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer(); this.logger.verbose("Preparing execution payload from engine", { slot: blockSlot, parentBlockRoot: toRootHex(parentBlockRoot), feeRecipient, }); // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/deneb/validator.md#constructing-the-beaconblockbody const prepareRes = await prepareExecutionPayload( this, this.logger, fork, parentBlockRoot, currentState.latestExecutionPayloadHeader.blockHash, safeBlockHash, finalizedBlockHash ?? ZERO_HASH_HEX, currentState, feeRecipient ); const {prepType, payloadId} = prepareRes; Object.assign(logMeta, {executionPayloadPrepType: prepType}); if (prepType !== PayloadPreparationType.Cached) { // Wait for 500ms to allow EL to add some txs to the payload // the pitfalls of this have been put forward here, but 500ms delay for block proposal // seems marginal even with unhealthy network // // See: https://discord.com/channels/595666850260713488/892088344438255616/1009882079632314469 await sleep(PAYLOAD_GENERATION_TIME_MS); } this.logger.verbose("Fetching execution payload from engine", {slot: blockSlot, payloadId}); const payloadRes = await this.executionEngine.getPayload(fork, payloadId); endExecutionPayload?.({ step: BlockProductionStep.executionPayload, }); return {...prepareRes, ...payloadRes}; })().catch((e) => { this.metrics?.blockPayload.payloadFetchErrors.inc(); throw e; }); const [engineRes, commonBlockBody] = await Promise.all([enginePromise, commonBlockBodyPromise]); blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<BlockType.Blinded>; { const {prepType, payloadId, executionPayload, blobsBundle, executionRequests} = engineRes; shouldOverrideBuilder = engineRes.shouldOverrideBuilder; (blockBody as BeaconBlockBody<ForkPostBellatrix & ForkPreGloas>).executionPayload = executionPayload; (produceResult as ProduceFullBellatrix).executionPayload = executionPayload; executionPayloadValue = engineRes.executionPayloadValue; Object.assign(logMeta, {transactions: executionPayload.transactions.length, shouldOverrideBuilder}); const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime); this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime); this.logger.verbose("Fetched execution payload from engine", { slot: blockSlot, executionPayloadValue, prepType, payloadId, fetchedTime, executionHeadBlockHash: toRootHex(engineRes.executionPayload.blockHash), }); if (executionPayload.transactions.length === 0) { this.metrics?.blockPayload.emptyPayloads.inc({prepType}); } if (ForkSeq[fork] >= ForkSeq.fulu) { if (blobsBundle === undefined) { throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`); } // NOTE: Even though the fulu.BlobsBundle type is superficially the same as deneb.BlobsBundle, it is NOT. // In fulu, proofs are _cell_ proofs, vs in deneb they are _blob_ proofs. const timer = this?.metrics?.peerDas.dataColumnSidecarComputationTime.startTimer(); const cells = blobsBundle.blobs.map((blob) => kzg.computeCells(blob)); timer?.(); if (this.opts.sanityCheckExecutionEngineBlobs) { const validationTimer = this.metrics?.peerDas.kzgVerificationDataColumnBatchTime.startTimer(); try { await validateCellsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, cells); } finally { validationTimer?.(); } } (blockBody as deneb.BeaconBlockBody).blobKzgCommitments = blobsBundle.commitments; (produceResult as ProduceFullFulu).blobsBundle = blobsBundle; (produceResult as ProduceFullFulu).cells = cells; Object.assign(logMeta, {blobs: blobsBundle.commitments.length}); } else if (ForkSeq[fork] >= ForkSeq.deneb) { if (blobsBundle === undefined) { throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`); } if (this.opts.sanityCheckExecutionEngineBlobs) { await validateBlobsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, blobsBundle.blobs); } (blockBody as deneb.BeaconBlockBody).blobKzgCommitments = blobsBundle.commitments; (produceResult as ProduceFullDeneb).blobsBundle = blobsBundle; Object.assign(logMeta, {blobs: blobsBundle.commitments.length}); } if (ForkSeq[fork] >= ForkSeq.electra) { if (executionRequests === undefined) { throw Error(`Missing executionRequests response from getPayload at fork=${fork}`); } (blockBody as electra.BeaconBlockBody).executionRequests = executionRequests; } } } } else { const commonBlockBody = await commonBlockBodyPromise; blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<T>; executionPayloadValue = BigInt(0); } const {graffiti, attestations, deposits, voluntaryExits, attesterSlashings, proposerSlashings} = blockBody; Object.assign(logMeta, { graffiti: fromGraffitiBytes(graffiti), attestations: attestations.length, deposits: deposits.length, voluntaryExits: voluntaryExits.length, attesterSlashings: attesterSlashings.length, proposerSlashings: proposerSlashings.length, }); if (isForkPostAltair(fork)) { const {syncAggregate} = blockBody as altair.BeaconBlockBody; Object.assign(logMeta, { syncAggregateParticipants: syncAggregate.syncCommitteeBits.getTrueBitIndexes().length, }); } if (ForkSeq[fork] >= ForkSeq.gloas) { const {blsToExecutionChanges, payloadAttestations} = blockBody as BeaconBlockBody<ForkPostGloas>; Object.assign(logMeta, { blsToExecutionChanges: blsToExecutionChanges.length, payloadAttestations: payloadAttestations.length, }); } else if (ForkSeq[fork] >= ForkSeq.capella) { const {blsToExecutionChanges, executionPayload} = blockBody as BeaconBlockBody<ForkPostCapella & ForkPreGloas>; Object.assign(logMeta, { blsToExecutionChanges: blsToExecutionChanges.length, }); // withdrawals are only available in full body if (blockType === BlockType.Full) { Object.assign(logMeta, { withdrawals: executionPayload.withdrawals.length, }); } } Object.assign(logMeta, {executionPayloadValue}); this.logger.verbose("Produced beacon block body", logMeta); return {body: blockBody as AssembledBodyType<T>, produceResult, executionPayloadValue, shouldOverrideBuilder}; } /** * Produce ExecutionPayload for post-merge. */ export async function prepareExecutionPayload( chain: { executionEngine: IExecutionEngine; config: ChainForkConfig; }, logger: Logger, fork: ForkPostBellatrix, parentBlockRoot: Root, parentBlockHash: Bytes32, safeBlockHash: RootHex, finalizedBlockHash: RootHex, /** * Post-gloas, when extending a full parent, callers must apply * parent execution payload first (see `withParentPayloadApplied`). */ state: IBeaconStateViewBellatrix, suggestedFeeRecipient: string ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime); const prevRandao = state.getRandaoMix(state.epoch); const payloadIdCached = chain.executionEngine.payloadIdCache.get({ headBlockHash: toRootHex(parentBlockHash), finalizedBlockHash, timestamp: numToQuantity(timestamp), prevRandao: toHex(prevRandao), suggestedFeeRecipient, }); // prepareExecutionPayload will throw error via notifyForkchoiceUpdate if // the EL returns Syncing on this request to prepare a payload // TODO: Handle only this case, DO NOT put a generic try / catch that discards all errors let payloadId: PayloadId | null; let prepType: PayloadPreparationType; if (payloadIdCached) { payloadId = payloadIdCached; prepType = PayloadPreparationType.Cached; } else { // If there was a payload assigned to this timestamp, it would imply that there some sort // of payload reorg, i.e. head, fee recipient or any other fcu param changed if (chain.executionEngine.payloadIdCache.hasPayload({timestamp: numToQuantity(timestamp)})) { prepType = PayloadPreparationType.Reorged; } else { prepType = PayloadPreparationType.Fresh; } const attributes: PayloadAttributes = preparePayloadAttributes(fork, chain, { prepareState: state, prepareSlot: state.slot, parentBlockRoot, parentBlockHash, feeRecipient: suggestedFeeRecipient, }); payloadId = await chain.executionEngine.notifyForkchoiceUpdate( fork, toRootHex(parentBlockHash), safeBlockHash, finalizedBlockHash, attributes ); logger.verbose("Prepared payload id from execution engine", {payloadId}); } // Should never happen, notifyForkchoiceUpdate() with payload attributes always // returns payloadId if (payloadId === null) { throw Error("notifyForkchoiceUpdate returned payloadId null"); } // We are only returning payloadId here because prepareExecutionPayload is also called from // prepareNextSlot, which is an advance call to execution engine to start building payload // Actual payload isn't produced till getPayload is called. return {payloadId, prepType}; } async function prepareExecutionPayloadHeader( chain: { executionBuilder?: IExecutionBuilder; config: ChainForkConfig; }, fork: ForkPostBellatrix, state: IBeaconStateViewBellatrix, proposerPubKey: BLSPubkey ): Promise<{ header: ExecutionPayloadHeader; executionPayloadValue: Wei; blobKzgCommitments?: deneb.BlobKzgCommitments; executionRequests?: electra.ExecutionRequests; }> { if (!chain.executionBuilder) { throw Error("executionBuilder required"); } const parentHash = state.latestExecutionPayloadHeader.blockHash; return chain.executionBuilder.getHeader(fork, state.slot, parentHash, proposerPubKey); } export function getPayloadAttributesForSSE( fork: ForkPostBellatrix, chain: { config: ChainForkConfig; forkChoice: IForkChoice; }, { prepareState, prepareSlot, parentBlockRoot, parentBlockHash, feeRecipient, }: { /** * Post-gloas, when extending a full parent, callers must apply * parent execution payload first (see `withParentPayloadApplied`). */ prepareState: IBeaconStateViewBellatrix; prepareSlot: Slot; parentBlockRoot: Root; parentBlockHash: Bytes32; feeRecipient: string; } ): SSEPayloadAttributes { const payloadAttributes = preparePayloadAttributes(fork, chain, { prepareState, prepareSlot, parentBlockRoot, parentBlockHash, feeRecipient, }); let parentBlockNumber: number; if (isForkPostGloas(fork)) { const parentBlock = chain.forkChoice.getBlockHexAndBlockHash( toRootHex(parentBlockRoot), toRootHex(parentBlockHash) ); if (parentBlock?.executionPayloadBlockHash == null) { throw Error(`Parent block not found in fork choice root=${toRootHex(parentBlockRoot)}`); } parentBlockNumber = parentBlock.executionPayloadNumber; } else { parentBlockNumber = prepareState.payloadBlockNumber; } const ssePayloadAttributes: SSEPayloadAttributes = { proposerIndex: prepareState.getBeaconProposer(prepareSlot), proposalSlot: prepareSlot, parentBlockNumber, parentBlockRoot, parentBlockHash, payloadAttributes, }; return ssePayloadAttributes; } function preparePayloadAttributes( fork: ForkPostBellatrix, chain: { config: ChainForkConfig; }, { prepareState, prepareSlot, parentBlockRoot, parentBlockHash, feeRecipient, }: { /** * Post-gloas, when extending a full parent, callers must apply * parent execution payload first (see `withParentPayloadApplied`). */ prepareState: IBeaconStateViewBellatrix; prepareSlot: Slot; parentBlockRoot: Root; parentBlockHash: Bytes32; feeRecipient: string; } ): SSEPayloadAttributes["payloadAttributes"] { const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime); const prevRandao = prepareState.getRandaoMix(prepareState.epoch); const payloadAttributes = { timestamp, prevRandao, suggestedFeeRecipient: feeRecipient, }; if (ForkSeq[fork] >= ForkSeq.capella) { if (!isStatePostCapella(prepareState)) { throw new Error("Expected Capella state for withdrawals"); } if (isStatePostGloas(prepareState)) { const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash); if (isExtendingPayload) { // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch // here means the caller did not apply parent payload to prepareState if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) { throw new Error("Expected state with parent execution payload applied for withdrawals"); } (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals; } else { // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch // already deducted from CL balances but never credited on the EL (the envelope // was not delivered). The next payload must carry those same withdrawals to // restore CL/EL consistency, otherwise validators permanently lose that balance. (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = prepareState.payloadExpectedWithdrawals; } } else { // withdrawals logic is now fork aware as it changes on electra fork post capella (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals; } } if (ForkSeq[fork] >= ForkSeq.deneb) { (payloadAttributes as deneb.SSEPayloadAttributes["payloadAttributes"]).parentBeaconBlockRoot = parentBlockRoot; } if (ForkSeq[fork] >= ForkSeq.gloas) { (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot; } return payloadAttributes; } export async function produceCommonBlockBody<T extends BlockType>( this: BeaconChain, blockType: T, currentState: IBeaconStateView, {randaoReveal, graffiti, slot, parentBlock}: BlockAttributes ): Promise<CommonBlockBody> { const stepsMetrics = blockType === BlockType.Full ? this.metrics?.executionBlockProductionTimeSteps : this.metrics?.builderBlockProductionTimeSteps; const fork = this.config.getForkName(slot); // TODO: // Iterate through the naive aggregation pool and ensure all the attestations from there // are included in the operation pool. // for (const attestation of db.attestationPool.getAll()) { // try { // opPool.insertAttestation(attestation); // } catch (e) { // // Don't stop block production if there's an error, just create a log. // logger.error("Attestation did not transfer to op pool", {}, e); // } // } const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics); const endAttestations = stepsMetrics?.startTimer(); const attestations = this.aggregatedAttestationPool.getAttestationsForBlock( fork, this.forkChoice, this.shufflingCache, currentState ); endAttestations?.({ step: BlockProductionStep.attestations, }); const blockBody: Omit<CommonBlockBody, "blsToExecutionChanges" | "syncAggregate"> = { randaoReveal, graffiti, // Eth1 data voting is no longer required since electra eth1Data: currentState.eth1Data, proposerSlashings, attesterSlashings, attestations, // Since electra, deposits are processed by the execution layer, // we no longer support handling deposits from earlier forks. deposits: [], voluntaryExits, }; if (ForkSeq[fork] >= ForkSeq.capella) { (blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges; } const endSyncAggregate = stepsMetrics?.startTimer(); if (ForkSeq[fork] >= ForkSeq.altair) { const parentBlockRoot = fromHex(parentBlock.blockRoot); const previousSlot = slot - 1; const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot); this.metrics?.production.producedSyncAggregateParticipants.observe( syncAggregate.syncCommitteeBits.getTrueBitIndexes().length ); (blockBody as CommonBlockBody).syncAggregate = syncAggregate; } endSyncAggregate?.({ step: BlockProductionStep.syncAggregate, }); return blockBody as CommonBlockBody; }