diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 839c1df7c463..f0f076c6c13a 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -50,7 +50,7 @@ import {RegenCaller} from "../../../chain/regen/index.js"; import {getValidatorStatus} from "../beacon/state/utils.js"; import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js"; import {SCHEDULER_LOOKAHEAD_FACTOR} from "../../../chain/prepareNextSlot.js"; -import {ChainEvent, CheckpointHex} from "../../../chain/index.js"; +import {ChainEvent, CheckpointHex, CommonBlockBody} from "../../../chain/index.js"; import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices} from "./utils.js"; /** @@ -287,7 +287,11 @@ export function getValidatorApi({ // as of now fee recipient checks can not be performed because builder does not return bid recipient { skipHeadChecksAndUpdate, - }: Omit & {skipHeadChecksAndUpdate?: boolean} = {} + commonBlockBody, + }: Omit & { + skipHeadChecksAndUpdate?: boolean; + commonBlockBody?: CommonBlockBody; + } = {} ): Promise { const version = config.getForkName(slot); if (!isForkExecution(version)) { @@ -323,6 +327,7 @@ export function getValidatorApi({ slot, randaoReveal, graffiti: toGraffitiBuffer(graffiti || ""), + commonBlockBody, }); metrics?.blockProductionSuccess.inc({source}); @@ -352,7 +357,11 @@ export function getValidatorApi({ feeRecipient, strictFeeRecipientCheck, skipHeadChecksAndUpdate, - }: Omit & {skipHeadChecksAndUpdate?: boolean} = {} + commonBlockBody, + }: Omit & { + skipHeadChecksAndUpdate?: boolean; + commonBlockBody?: CommonBlockBody; + } = {} ): Promise { const source = ProducedBlockSource.engine; metrics?.blockProductionRequests.inc({source}); @@ -376,6 +385,7 @@ export function getValidatorApi({ randaoReveal, graffiti: toGraffitiBuffer(graffiti || ""), feeRecipient, + commonBlockBody, }); const version = config.getForkName(block.slot); if (strictFeeRecipientCheck && feeRecipient && isForkExecution(version)) { @@ -456,7 +466,7 @@ export function getValidatorApi({ chain.executionBuilder !== undefined && builderSelection !== routes.validator.BuilderSelection.ExecutionOnly; - logger.verbose("Assembling block with produceEngineOrBuilderBlock ", { + const loggerContext = { fork, builderSelection, slot, @@ -464,7 +474,16 @@ export function getValidatorApi({ strictFeeRecipientCheck, // winston logger doesn't like bigint builderBoostFactor: `${builderBoostFactor}`, + }; + + logger.verbose("Assembling block with produceEngineOrBuilderBlock", loggerContext); + const commonBlockBody = await chain.produceCommonBlockBody({ + slot, + randaoReveal, + graffiti: toGraffitiBuffer(graffiti || ""), }); + logger.debug("Produced common block body", loggerContext); + // Start calls for building execution and builder blocks const blindedBlockPromise = isBuilderEnabled ? // can't do fee recipient checks as builder bid doesn't return feeRecipient as of now @@ -472,6 +491,7 @@ export function getValidatorApi({ feeRecipient, // skip checking and recomputing head in these individual produce calls skipHeadChecksAndUpdate: true, + commonBlockBody, }).catch((e) => { logger.error("produceBuilderBlindedBlock failed to produce block", {slot}, e); return null; @@ -494,6 +514,7 @@ export function getValidatorApi({ strictFeeRecipientCheck, // skip checking and recomputing head in these individual produce calls skipHeadChecksAndUpdate: true, + commonBlockBody, }).catch((e) => { logger.error("produceEngineFullBlockOrContents failed to produce block", {slot}, e); return null; diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index ac2f97128c16..520b20b820fc 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -44,7 +44,7 @@ import {ensureDir, writeIfNotExist} from "../util/file.js"; import {isOptimisticBlock} from "../util/forkChoice.js"; import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js"; import {ChainEventEmitter, ChainEvent} from "./emitter.js"; -import {IBeaconChain, ProposerPreparationData, BlockHash, StateGetOpts} from "./interface.js"; +import {IBeaconChain, ProposerPreparationData, BlockHash, StateGetOpts, CommonBlockBody} from "./interface.js"; import {IChainOptions} from "./options.js"; import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js"; import {initializeForkChoice} from "./forkChoice/index.js"; @@ -73,7 +73,7 @@ import {SeenBlockAttesters} from "./seenCache/seenBlockAttesters.js"; import {BeaconProposerCache} from "./beaconProposerCache.js"; import {CheckpointBalancesCache} from "./balancesCache.js"; import {AssembledBlockType, BlobsResultType, BlockType} from "./produceBlock/index.js"; -import {BlockAttributes, produceBlockBody} from "./produceBlock/produceBlockBody.js"; +import {BlockAttributes, produceBlockBody, produceCommonBlockBody} from "./produceBlock/produceBlockBody.js"; import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js"; import {BlockInput} from "./blocks/types.js"; import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; @@ -463,14 +463,35 @@ export class BeaconChain implements IBeaconChain { return {block: data, executionOptimistic: isOptimisticBlock(block)}; } // If block is not found in hot db, try cold db since there could be an archive cycle happening - // TODO: Add a lock to the archiver to have determinstic behaviour on where are blocks + // TODO: Add a lock to the archiver to have deterministic behavior on where are blocks } const data = await this.db.blockArchive.getByRoot(fromHexString(root)); return data && {block: data, executionOptimistic: false}; } - produceBlock(blockAttributes: BlockAttributes): Promise<{ + async produceCommonBlockBody(blockAttributes: BlockAttributes): Promise { + const {slot} = blockAttributes; + const head = this.forkChoice.getHead(); + const state = await this.regen.getBlockSlotState( + head.blockRoot, + slot, + {dontTransferCache: true}, + RegenCaller.produceBlock + ); + const parentBlockRoot = fromHexString(head.blockRoot); + + // TODO: To avoid breaking changes for metric define this attribute + const blockType = BlockType.Full; + + return produceCommonBlockBody.call(this, blockType, state, { + ...blockAttributes, + parentBlockRoot, + parentSlot: slot - 1, + }); + } + + produceBlock(blockAttributes: BlockAttributes & {commonBlockBody?: CommonBlockBody}): Promise<{ block: allForks.BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei; @@ -479,7 +500,7 @@ export class BeaconChain implements IBeaconChain { return this.produceBlockWrapper(BlockType.Full, blockAttributes); } - produceBlindedBlock(blockAttributes: BlockAttributes): Promise<{ + produceBlindedBlock(blockAttributes: BlockAttributes & {commonBlockBody?: CommonBlockBody}): Promise<{ block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei; @@ -489,7 +510,7 @@ export class BeaconChain implements IBeaconChain { async produceBlockWrapper( blockType: T, - {randaoReveal, graffiti, slot, feeRecipient}: BlockAttributes + {randaoReveal, graffiti, slot, feeRecipient, commonBlockBody}: BlockAttributes & {commonBlockBody?: CommonBlockBody} ): Promise<{ block: AssembledBlockType; executionPayloadValue: Wei; @@ -520,6 +541,7 @@ export class BeaconChain implements IBeaconChain { parentBlockRoot, proposerIndex, proposerPubKey, + commonBlockBody, } ); diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index a2f7fba34093..6e932b25c50e 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -11,6 +11,8 @@ import { deneb, Wei, Gwei, + capella, + altair, } from "@lodestar/types"; import { BeaconStateAllForks, @@ -154,13 +156,14 @@ export interface IBeaconChain { getContents(beaconBlock: deneb.BeaconBlock): deneb.Contents; - produceBlock(blockAttributes: BlockAttributes): Promise<{ + produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; + produceBlock(blockAttributes: BlockAttributes & {commonBlockBody?: CommonBlockBody}): Promise<{ block: allForks.BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei; shouldOverrideBuilder?: boolean; }>; - produceBlindedBlock(blockAttributes: BlockAttributes): Promise<{ + produceBlindedBlock(blockAttributes: BlockAttributes & {commonBlockBody?: CommonBlockBody}): Promise<{ block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei; @@ -204,3 +207,7 @@ export type SSZObjectType = | "signedAggregatedAndProof" | "syncCommittee" | "contributionAndProof"; + +export type CommonBlockBody = phase0.BeaconBlockBody & + Pick & + Pick; diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 0b6ff7b1316b..b25b71514a71 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -1,8 +1,6 @@ import { Bytes32, - phase0, allForks, - altair, Root, RootHex, Slot, @@ -35,6 +33,7 @@ import {PayloadId, IExecutionEngine, IExecutionBuilder, PayloadAttributes} from import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; import {IEth1ForBlockProduction} from "../../eth1/index.js"; import {numToQuantity} from "../../eth1/provider/utils.js"; +import {CommonBlockBody} from "../interface.js"; import {validateBlobsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js"; // Time to provide the EL to generate a payload from new payload id @@ -94,20 +93,12 @@ export async function produceBlockBody( this: BeaconChain, blockType: T, currentState: CachedBeaconStateAllForks, - { - randaoReveal, - graffiti, - slot: blockSlot, - feeRecipient: requestedFeeRecipient, - parentSlot, - parentBlockRoot, - proposerIndex, - proposerPubKey, - }: BlockAttributes & { + blockAttr: BlockAttributes & { parentSlot: Slot; parentBlockRoot: Root; proposerIndex: ValidatorIndex; proposerPubKey: BLSPubkey; + commonBlockBody?: CommonBlockBody; } ): Promise<{ body: AssembledBodyType; @@ -115,6 +106,14 @@ export async function produceBlockBody( executionPayloadValue: Wei; shouldOverrideBuilder?: boolean; }> { + const { + slot: blockSlot, + feeRecipient: requestedFeeRecipient, + parentBlockRoot, + proposerIndex, + proposerPubKey, + commonBlockBody, + } = blockAttr; // Type-safe for blobs variable. Translate 'null' value into 'preDeneb' enum // TODO: Not ideal, but better than just using null. // TODO: Does not guarantee that preDeneb enum goes with a preDeneb block @@ -131,63 +130,17 @@ export async function produceBlockBody( slot: blockSlot, }; this.logger.verbose("Producing beacon block body", logMeta); - - // 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 stepsMetrics = blockType === BlockType.Full ? this.metrics?.executionBlockProductionTimeSteps : this.metrics?.builderBlockProductionTimeSteps; - const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = - this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics); - - const endAttestations = stepsMetrics?.startTimer(); - const attestations = this.aggregatedAttestationPool.getAttestationsForBlock(this.forkChoice, currentState); - endAttestations?.({ - step: BlockProductionStep.attestations, - }); - - const endEth1DataAndDeposits = stepsMetrics?.startTimer(); - const {eth1Data, deposits} = await this.eth1.getEth1DataAndDeposits(currentState); - endEth1DataAndDeposits?.({ - step: BlockProductionStep.eth1DataAndDeposits, - }); + const blockBody = commonBlockBody + ? Object.assign({}, commonBlockBody) + : await produceCommonBlockBody.call(this, blockType, currentState, blockAttr); - const blockBody: phase0.BeaconBlockBody = { - randaoReveal, - graffiti, - eth1Data, - proposerSlashings, - attesterSlashings, - attestations, - deposits, - voluntaryExits, - }; - - const blockEpoch = computeEpochAtSlot(blockSlot); - - const endSyncAggregate = stepsMetrics?.startTimer(); - if (blockEpoch >= this.config.ALTAIR_FORK_EPOCH) { - const syncAggregate = this.syncContributionAndProofPool.getAggregate(parentSlot, parentBlockRoot); - this.metrics?.production.producedSyncAggregateParticipants.observe( - syncAggregate.syncCommitteeBits.getTrueBitIndexes().length - ); - (blockBody as altair.BeaconBlockBody).syncAggregate = syncAggregate; - } - endSyncAggregate?.({ - step: BlockProductionStep.syncAggregate, - }); + const {attestations, deposits, voluntaryExits, attesterSlashings, proposerSlashings, blsToExecutionChanges} = + blockBody; Object.assign(logMeta, { attestations: attestations.length, @@ -317,6 +270,7 @@ export async function produceBlockBody( prepType, payloadId, fetchedTime, + executionHeadBlockHash: toHex(engineRes.executionPayload.blockHash), }); if (executionPayload.transactions.length === 0) { this.metrics?.blockPayload.emptyPayloads.inc({prepType}); @@ -373,8 +327,6 @@ export async function produceBlockBody( }); if (ForkSeq[fork] >= ForkSeq.capella) { - // TODO: blsToExecutionChanges should be passed in the produceBlock call - (blockBody as capella.BeaconBlockBody).blsToExecutionChanges = blsToExecutionChanges; Object.assign(logMeta, { blsToExecutionChanges: blsToExecutionChanges.length, }); @@ -616,4 +568,81 @@ function preparePayloadAttributes( return payloadAttributes; } -/** process_sync_committee_contributions is implemented in syncCommitteeContribution.getSyncAggregate */ +export async function produceCommonBlockBody( + this: BeaconChain, + blockType: T, + currentState: CachedBeaconStateAllForks, + { + randaoReveal, + graffiti, + slot, + parentSlot, + parentBlockRoot, + }: BlockAttributes & { + parentSlot: Slot; + parentBlockRoot: Root; + } +): Promise { + const stepsMetrics = + blockType === BlockType.Full + ? this.metrics?.executionBlockProductionTimeSteps + : this.metrics?.builderBlockProductionTimeSteps; + + const blockEpoch = computeEpochAtSlot(slot); + const fork = currentState.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(this.forkChoice, currentState); + endAttestations?.({ + step: BlockProductionStep.attestations, + }); + + const endEth1DataAndDeposits = stepsMetrics?.startTimer(); + const {eth1Data, deposits} = await this.eth1.getEth1DataAndDeposits(currentState); + endEth1DataAndDeposits?.({ + step: BlockProductionStep.eth1DataAndDeposits, + }); + + const blockBody: Omit = { + randaoReveal, + graffiti, + eth1Data, + proposerSlashings, + attesterSlashings, + attestations, + deposits, + voluntaryExits, + }; + + if (ForkSeq[fork] >= ForkSeq.capella) { + (blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges; + } + + const endSyncAggregate = stepsMetrics?.startTimer(); + if (blockEpoch >= this.config.ALTAIR_FORK_EPOCH) { + const syncAggregate = this.syncContributionAndProofPool.getAggregate(parentSlot, parentBlockRoot); + this.metrics?.production.producedSyncAggregateParticipants.observe( + syncAggregate.syncCommitteeBits.getTrueBitIndexes().length + ); + (blockBody as CommonBlockBody).syncAggregate = syncAggregate; + } + endSyncAggregate?.({ + step: BlockProductionStep.syncAggregate, + }); + + return blockBody as CommonBlockBody; +} diff --git a/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts b/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts index 3c5dacc9c971..c72d22471ce8 100644 --- a/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts +++ b/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts @@ -79,6 +79,7 @@ vi.mock("../../src/chain/index.js", async (requireActual) => { // @ts-expect-error beaconProposerCache: new BeaconProposerCache(), shufflingCache: new ShufflingCache(), + produceCommonBlockBody: vi.fn(), produceBlock: vi.fn(), produceBlindedBlock: vi.fn(), getCanonicalBlockAtSlot: vi.fn(), diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts index 3a87b709b741..f1aa2cb791df 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts @@ -9,6 +9,7 @@ import {getValidatorApi} from "../../../../../src/api/impl/validator/index.js"; import {testLogger} from "../../../../utils/logger.js"; import {ApiImplTestModules, setupApiImplTestServer} from "../../../../__mocks__/apiMocks.js"; import {ExecutionBuilderHttp} from "../../../../../src/execution/builder/http.js"; +import {CommonBlockBody} from "../../../../../src/chain/interface.js"; /* eslint-disable @typescript-eslint/naming-convention */ describe("api/validator - produceBlockV3", function () { @@ -100,6 +101,21 @@ describe("api/validator - produceBlockV3", function () { const api = getValidatorApi(modules); if (enginePayloadValue !== null) { + const commonBlockBody: CommonBlockBody = { + attestations: fullBlock.body.attestations, + attesterSlashings: fullBlock.body.attesterSlashings, + deposits: fullBlock.body.deposits, + proposerSlashings: fullBlock.body.proposerSlashings, + eth1Data: fullBlock.body.eth1Data, + graffiti: fullBlock.body.graffiti, + randaoReveal: fullBlock.body.randaoReveal, + voluntaryExits: fullBlock.body.voluntaryExits, + blsToExecutionChanges: [], + syncAggregate: fullBlock.body.syncAggregate, + }; + + chainStub.produceCommonBlockBody.mockResolvedValue(commonBlockBody); + chainStub.produceBlock.mockResolvedValue({ block: fullBlock, executionPayloadValue: BigInt(enginePayloadValue),