diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 4743c9cd2803..a1a9c75d041a 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -714,10 +714,19 @@ export function getReturnTypes(): ReturnTypes { produceBlock: ContainerData(ssz.phase0.BeaconBlock), produceBlockV2: produceBlockOrContents, produceBlockV3: { - toJson: (data) => - data.executionPayloadBlinded === true - ? produceBlindedBlockOrContents.toJson(data) - : produceBlockOrContents.toJson(data), + toJson: (data) => { + if (data.executionPayloadBlinded) { + return { + execution_payload_blinded: true, + ...(produceBlindedBlockOrContents.toJson(data) as Record), + }; + } else { + return { + execution_payload_blinded: false, + ...(produceBlockOrContents.toJson(data) as Record), + }; + } + }, fromJson: (data) => { if ((data as {execution_payload_blinded: true}).execution_payload_blinded) { return {executionPayloadBlinded: true, ...produceBlindedBlockOrContents.fromJson(data)}; diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index a99b87106f14..d59e884b9a66 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -428,7 +428,7 @@ export function getValidatorApi({ // Callback to log the race events for better debugging capability (event: RaceEvent, delayMs: number, index?: number) => { const eventRef = index !== undefined ? {source: promisesOrder[index]} : {}; - logger.debug("Block production race (builder vs execution)", { + logger.verbose("Block production race (builder vs execution)", { event, ...eventRef, delayMs, @@ -481,7 +481,7 @@ export function getValidatorApi({ selectedSource = ProducedBlockSource.builder; } } - logger.debug(`Selected ${selectedSource} block`, { + logger.verbose(`Selected ${selectedSource} block`, { builderSelection, // winston logger doesn't like bigint enginePayloadValue: `${enginePayloadValue}`, @@ -489,13 +489,13 @@ export function getValidatorApi({ }); } else if (fullBlock && !blindedBlock) { selectedSource = ProducedBlockSource.engine; - logger.debug("Selected engine block: no builder block produced", { + logger.verbose("Selected engine block: no builder block produced", { // winston logger doesn't like bigint enginePayloadValue: `${enginePayloadValue}`, }); } else if (blindedBlock && !fullBlock) { selectedSource = ProducedBlockSource.builder; - logger.debug("Selected builder block: no engine block produced", { + logger.verbose("Selected builder block: no engine block produced", { // winston logger doesn't like bigint builderPayloadValue: `${builderPayloadValue}`, }); diff --git a/packages/params/src/forkName.ts b/packages/params/src/forkName.ts index f6a31c458aee..b7428707050e 100644 --- a/packages/params/src/forkName.ts +++ b/packages/params/src/forkName.ts @@ -27,19 +27,19 @@ export function isForkLightClient(fork: ForkName): fork is ForkLightClient { } export type ForkPreExecution = ForkPreLightClient | ForkName.altair; -export type ForkExecution = Exclude; +export type ForkExecution = Exclude; export function isForkExecution(fork: ForkName): fork is ForkExecution { return isForkLightClient(fork) && fork !== ForkName.altair; } export type ForkPreWithdrawals = ForkPreExecution | ForkName.bellatrix; -export type ForkWithdrawals = Exclude; +export type ForkWithdrawals = Exclude; export function isForkWithdrawals(fork: ForkName): fork is ForkWithdrawals { return isForkExecution(fork) && fork !== ForkName.bellatrix; } export type ForkPreBlobs = ForkPreWithdrawals | ForkName.capella; -export type ForkBlobs = Exclude; -export function isForkBlobs(fork: ForkName): fork is ForkBlobs { +export type ForkBlobs = Exclude; +export function isForkBlobs(fork: ForkName): fork is ForkName.deneb { return isForkWithdrawals(fork) && fork !== ForkName.capella; } diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index 49061ae1ddba..5d8998377865 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -4,16 +4,13 @@ import { Slot, BLSSignature, allForks, - bellatrix, - capella, isBlindedBeaconBlock, - Wei, ProducedBlockSource, deneb, } from "@lodestar/types"; import {ChainForkConfig} from "@lodestar/config"; -import {ForkName} from "@lodestar/params"; -import {extendError, prettyBytes, racePromisesWithCutoff, RaceEvent} from "@lodestar/utils"; +import {ForkPreBlobs, ForkBlobs} from "@lodestar/params"; +import {extendError, prettyBytes} from "@lodestar/utils"; import { Api, ApiError, @@ -21,8 +18,6 @@ import { isBlindedBlockContents, SignedBlindedBlockContents, SignedBlockContents, - BlockContents, - BlindedBlockContents, } from "@lodestar/api"; import {IClock, LoggerVc} from "../util/index.js"; import {PubkeyHex} from "../types.js"; @@ -43,9 +38,9 @@ const MAX_DECIMAL_FACTOR = BigInt("100000"); * So lowering the cutoff to 2 sec from 3 seconds to publish faster for successful proposal * as proposals post 4 seconds into the slot seems to be not being included */ -const BLOCK_PRODUCTION_RACE_CUTOFF_MS = 2_000; -/** Overall timeout for execution and block production apis */ -const BLOCK_PRODUCTION_RACE_TIMEOUT_MS = 12_000; +// const BLOCK_PRODUCTION_RACE_CUTOFF_MS = 2_000; +// /** Overall timeout for execution and block production apis */ +// const BLOCK_PRODUCTION_RACE_TIMEOUT_MS = 12_000; type ProduceBlockOpts = { expectedFeeRecipient: string; @@ -54,6 +49,32 @@ type ProduceBlockOpts = { builderSelection: BuilderSelection; }; +type FullOrBlindedBlockWithContents = + | { + version: ForkPreBlobs; + block: allForks.BeaconBlock; + blobs: null; + executionPayloadBlinded: false; + } + | { + version: ForkBlobs; + block: allForks.BeaconBlock; + blobs: deneb.BlobSidecars; + executionPayloadBlinded: false; + } + | { + version: ForkPreBlobs; + block: allForks.BlindedBeaconBlock; + blobs: null; + executionPayloadBlinded: true; + } + | { + version: ForkBlobs; + block: allForks.BlindedBeaconBlock; + blobs: deneb.BlindedBlobSidecars; + executionPayloadBlinded: true; + }; + /** * Service that sets up and handles validator block proposal duties. */ @@ -143,7 +164,7 @@ export class BlockProposingService { const signedBlockPromise = this.validatorStore.signBlock(pubkey, blockContents.block, slot); const signedBlobPromises = - blockContents.blobs !== undefined + blockContents.blobs !== null ? blockContents.blobs.map((blob) => this.validatorStore.signBlob(pubkey, blob, slot)) : undefined; let signedBlock: allForks.FullOrBlindedSignedBeaconBlock, @@ -194,259 +215,56 @@ export class BlockProposingService { randaoReveal: BLSSignature, graffiti: string, {expectedFeeRecipient, strictFeeRecipientCheck, isBuilderEnabled, builderSelection}: ProduceBlockOpts - ): Promise< - {block: allForks.FullOrBlindedBeaconBlock; blobs?: allForks.FullOrBlindedBlobSidecars} & { - debugLogCtx: Record; - } - > => { - // Start calls for building execution and builder blocks - const blindedBlockPromise = isBuilderEnabled ? this.produceBlindedBlock(slot, randaoReveal, graffiti) : null; - const fullBlockPromise = - // At any point either the builder or execution or both flows should be active. - // - // Ideally such a scenario should be prevented on startup, but proposerSettingsFile or keymanager - // configurations could cause a validator pubkey to have builder disabled with builder selection builder only - // (TODO: independently make sure such an options update is not successful for a validator pubkey) - // - // So if builder is disabled ignore builder selection of builderonly if caused by user mistake - !isBuilderEnabled || builderSelection !== BuilderSelection.BuilderOnly - ? this.produceBlock(slot, randaoReveal, graffiti, expectedFeeRecipient) - : null; - - let blindedBlock, fullBlock; - if (blindedBlockPromise !== null && fullBlockPromise !== null) { - // reference index of promises in the race - const promisesOrder = [ProducedBlockSource.builder, ProducedBlockSource.engine]; - [blindedBlock, fullBlock] = await racePromisesWithCutoff<{ - block: allForks.FullOrBlindedBeaconBlock; - blobs?: allForks.FullOrBlindedBlobSidecars; - executionPayloadValue: Wei; - }>( - [blindedBlockPromise, fullBlockPromise], - BLOCK_PRODUCTION_RACE_CUTOFF_MS, - BLOCK_PRODUCTION_RACE_TIMEOUT_MS, - // Callback to log the race events for better debugging capability - (event: RaceEvent, delayMs: number, index?: number) => { - const eventRef = index !== undefined ? {source: promisesOrder[index]} : {}; - this.logger.debug("Block production race (builder vs execution)", { - event, - ...eventRef, - delayMs, - cutoffMs: BLOCK_PRODUCTION_RACE_CUTOFF_MS, - timeoutMs: BLOCK_PRODUCTION_RACE_TIMEOUT_MS, - }); - } - ); - if (blindedBlock instanceof Error) { - // error here means race cutoff exceeded - this.logger.error("Failed to produce builder block", {}, blindedBlock); - blindedBlock = null; - } - if (fullBlock instanceof Error) { - this.logger.error("Failed to produce execution block", {}, fullBlock); - fullBlock = null; - } - } else if (blindedBlockPromise !== null && fullBlockPromise === null) { - blindedBlock = await blindedBlockPromise; - fullBlock = null; - } else if (blindedBlockPromise === null && fullBlockPromise !== null) { - blindedBlock = null; - fullBlock = await fullBlockPromise; - } else { - throw Error( - `Internal Error: Neither builder nor execution proposal flow activated isBuilderEnabled=${isBuilderEnabled} builderSelection=${builderSelection}` - ); - } - - const builderPayloadValue = blindedBlock?.executionPayloadValue ?? BigInt(0); - const enginePayloadValue = fullBlock?.executionPayloadValue ?? BigInt(0); - - const feeRecipientCheck = {expectedFeeRecipient, strictFeeRecipientCheck}; - - if (fullBlock && blindedBlock) { - let selectedSource: ProducedBlockSource; - let selectedBlock; - switch (builderSelection) { - case BuilderSelection.MaxProfit: { - // If executionPayloadValues are zero, than choose builder as most likely beacon didn't provide executionPayloadValue - // and builder blocks are most likely thresholded by a min bid - if (enginePayloadValue >= builderPayloadValue && enginePayloadValue !== BigInt(0)) { - selectedSource = ProducedBlockSource.engine; - selectedBlock = fullBlock; - } else { - selectedSource = ProducedBlockSource.builder; - selectedBlock = blindedBlock; - } - break; - } - - // For everything else just select the builder - default: { - selectedSource = ProducedBlockSource.builder; - selectedBlock = blindedBlock; - } - } - this.logger.debug(`Selected ${selectedSource} block`, { - builderSelection, - // winston logger doesn't like bigint - enginePayloadValue: `${enginePayloadValue}`, - builderPayloadValue: `${builderPayloadValue}`, - }); - return this.getBlockWithDebugLog(selectedBlock, selectedSource, feeRecipientCheck); - } else if (fullBlock && !blindedBlock) { - this.logger.debug("Selected engine block: no builder block produced", { - // winston logger doesn't like bigint - enginePayloadValue: `${enginePayloadValue}`, - }); - return this.getBlockWithDebugLog(fullBlock, ProducedBlockSource.engine, feeRecipientCheck); - } else if (blindedBlock && !fullBlock) { - this.logger.debug("Selected builder block: no engine block produced", { - // winston logger doesn't like bigint - builderPayloadValue: `${builderPayloadValue}`, - }); - return this.getBlockWithDebugLog(blindedBlock, ProducedBlockSource.builder, feeRecipientCheck); - } else { - throw Error("Failed to produce engine or builder block"); - } - }; + ): Promise}> => { + const res = await this.api.validator.produceBlockV3(slot, randaoReveal, graffiti, expectedFeeRecipient); + ApiError.assert(res, "Failed to produce block: validator.produceBlockV2"); + const {response} = res; - private getBlockWithDebugLog( - fullOrBlindedBlock: { - block: allForks.FullOrBlindedBeaconBlock; - executionPayloadValue: Wei; - blobs?: allForks.FullOrBlindedBlobSidecars; - }, - source: ProducedBlockSource, - {expectedFeeRecipient, strictFeeRecipientCheck}: {expectedFeeRecipient: string; strictFeeRecipientCheck: boolean} - ): {block: allForks.FullOrBlindedBeaconBlock; blobs?: allForks.FullOrBlindedBlobSidecars} & { - debugLogCtx: Record; - } { const debugLogCtx = { - source: source, + source: response.executionPayloadBlinded ? ProducedBlockSource.builder : ProducedBlockSource.engine, // winston logger doesn't like bigint - executionPayloadValue: `${formatBigDecimal( - fullOrBlindedBlock.executionPayloadValue, - ETH_TO_WEI, - MAX_DECIMAL_FACTOR - )} ETH`, + executionPayloadValue: `${formatBigDecimal(response.executionPayloadValue, ETH_TO_WEI, MAX_DECIMAL_FACTOR)} ETH`, + // TODO PR: should be used in api call instead of adding in log + strictFeeRecipientCheck, + isBuilderEnabled, + builderSelection, }; - const blockFeeRecipient = (fullOrBlindedBlock.block as bellatrix.BeaconBlock).body.executionPayload?.feeRecipient; - const feeRecipient = blockFeeRecipient !== undefined ? toHexString(blockFeeRecipient) : undefined; - if (source === ProducedBlockSource.engine) { - if (feeRecipient !== undefined) { - if (feeRecipient !== expectedFeeRecipient && strictFeeRecipientCheck) { - throw Error(`Invalid feeRecipient=${feeRecipient}, expected=${expectedFeeRecipient}`); - } - } - } - - const transactions = (fullOrBlindedBlock.block as bellatrix.BeaconBlock).body.executionPayload?.transactions - ?.length; - const withdrawals = (fullOrBlindedBlock.block as capella.BeaconBlock).body.executionPayload?.withdrawals?.length; - - // feeRecipient, transactions or withdrawals can end up undefined - Object.assign( - debugLogCtx, - feeRecipient !== undefined ? {feeRecipient} : {}, - transactions !== undefined ? {transactions} : {}, - withdrawals !== undefined ? {withdrawals} : {} - ); - Object.assign(debugLogCtx, fullOrBlindedBlock.blobs !== undefined ? {blobs: fullOrBlindedBlock.blobs.length} : {}); - - return {...fullOrBlindedBlock, blobs: fullOrBlindedBlock.blobs, debugLogCtx}; - } - - /** Wrapper around the API's different methods for producing blocks across forks */ - private produceBlock = async ( - slot: Slot, - randaoReveal: BLSSignature, - graffiti: string, - expectedFeeRecipient?: string - ): Promise<{block: allForks.BeaconBlock; blobs?: deneb.BlobSidecars; executionPayloadValue: Wei}> => { - const fork = this.config.getForkName(slot); - switch (fork) { - case ForkName.phase0: { - const res = await this.api.validator.produceBlock(slot, randaoReveal, graffiti); - ApiError.assert(res, "Failed to produce block: validator.produceBlock"); - const {data: block} = res.response; - return {block, executionPayloadValue: BigInt(0)}; - } - - // All subsequent forks are expected to use v2 too - case ForkName.altair: - case ForkName.bellatrix: - case ForkName.capella: { - const res = await this.api.validator.produceBlockV2(slot, randaoReveal, graffiti, expectedFeeRecipient); - ApiError.assert(res, "Failed to produce block: validator.produceBlockV2"); - - const {response} = res; - if (isBlockContents(response.data)) { - throw Error(`Invalid BlockContents response at fork=${fork}`); - } - const {data: block, executionPayloadValue} = response as { - data: allForks.BeaconBlock; - executionPayloadValue: Wei; - }; - return {block, executionPayloadValue}; + let fullOrBlindedBlockWithContents: FullOrBlindedBlockWithContents; + if (response.executionPayloadBlinded) { + if (isBlindedBlockContents(response.data)) { + fullOrBlindedBlockWithContents = { + block: response.data.blindedBlock, + blobs: response.data.blindedBlobSidecars, + version: response.version, + executionPayloadBlinded: true, + } as FullOrBlindedBlockWithContents; + } else { + fullOrBlindedBlockWithContents = { + block: response.data, + blobs: null, + version: response.version, + executionPayloadBlinded: true, + } as FullOrBlindedBlockWithContents; } - - case ForkName.deneb: - default: { - const res = await this.api.validator.produceBlockV2(slot, randaoReveal, graffiti, expectedFeeRecipient); - ApiError.assert(res, "Failed to produce block: validator.produceBlockV2"); - - const {response} = res; - if (!isBlockContents(response.data)) { - throw Error(`Expected BlockContents response at fork=${fork}`); - } - const { - data: {block, blobSidecars: blobs}, - executionPayloadValue, - } = response as {data: BlockContents; executionPayloadValue: Wei}; - return {block, blobs, executionPayloadValue}; + } else { + if (isBlockContents(response.data)) { + fullOrBlindedBlockWithContents = { + block: response.data.block, + blobs: response.data.blobSidecars, + version: response.version, + executionPayloadBlinded: false, + } as FullOrBlindedBlockWithContents; + } else { + fullOrBlindedBlockWithContents = { + block: response.data, + blobs: null, + version: response.version, + executionPayloadBlinded: false, + } as FullOrBlindedBlockWithContents; } } - }; - private produceBlindedBlock = async ( - slot: Slot, - randaoReveal: BLSSignature, - graffiti: string - ): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei; blobs?: deneb.BlindedBlobSidecars}> => { - const res = await this.api.validator.produceBlindedBlock(slot, randaoReveal, graffiti); - ApiError.assert(res, "Failed to produce block: validator.produceBlindedBlock"); - const {response} = res; - - const fork = this.config.getForkName(slot); - switch (fork) { - case ForkName.phase0: - case ForkName.altair: - throw Error(`BlindedBlock functionality not applicable at fork=${fork}`); - - case ForkName.bellatrix: - case ForkName.capella: { - if (isBlindedBlockContents(response.data)) { - throw Error(`Invalid BlockContents response at fork=${fork}`); - } - const {data: block, executionPayloadValue} = response as { - data: allForks.BlindedBeaconBlock; - executionPayloadValue: Wei; - }; - return {block, executionPayloadValue}; - } - - case ForkName.deneb: - default: { - if (!isBlindedBlockContents(response.data)) { - throw Error(`Expected BlockContents response at fork=${fork}`); - } - const { - data: {blindedBlock: block, blindedBlobSidecars: blobs}, - executionPayloadValue, - } = response as {data: BlindedBlockContents; executionPayloadValue: Wei}; - return {block, blobs, executionPayloadValue}; - } - } + return {...fullOrBlindedBlockWithContents, debugLogCtx}; }; }