From 9dd29957ffd11c18e23908d5226d1e1458f88c16 Mon Sep 17 00:00:00 2001 From: harkamal Date: Thu, 21 Dec 2023 19:24:04 +0530 Subject: [PATCH] feat: allow validator to request blinded versions for locally produced and selected blocks --- packages/api/src/beacon/routes/validator.ts | 29 +- .../src/api/impl/validator/index.ts | 388 ++++++++++-------- packages/cli/src/cmds/validator/handler.ts | 1 + packages/cli/src/cmds/validator/options.ts | 7 + packages/validator/src/services/block.ts | 29 +- .../validator/src/services/validatorStore.ts | 2 + packages/validator/src/validator.ts | 2 + 7 files changed, 286 insertions(+), 172 deletions(-) diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index f7a731783681..cedc0deff52c 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -19,6 +19,7 @@ import { SubcommitteeIndex, Wei, Gwei, + ProducedBlockSource, } from "@lodestar/types"; import {ApiClientResponse} from "../../interfaces.js"; import {HttpStatusCode} from "../../utils/client/httpStatusCode.js"; @@ -53,6 +54,7 @@ export type ExtraProduceBlockOps = { feeRecipient?: string; builderSelection?: BuilderSelection; strictFeeRecipientCheck?: boolean; + blindedLocal?: boolean; }; export type ProduceBlockOrContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Gwei} & ( @@ -64,9 +66,10 @@ export type ProduceBlindedBlockRes = {executionPayloadValue: Wei; consensusBlock version: ForkExecution; }; -export type ProduceFullOrBlindedBlockOrContentsRes = +export type ProduceFullOrBlindedBlockOrContentsRes = {executionPayloadSource: ProducedBlockSource} & ( | (ProduceBlockOrContentsRes & {executionPayloadBlinded: false}) - | (ProduceBlindedBlockRes & {executionPayloadBlinded: true}); + | (ProduceBlindedBlockRes & {executionPayloadBlinded: true}) +); // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes @@ -485,6 +488,7 @@ export type ReqTypes = { fee_recipient?: string; builder_selection?: string; strict_fee_recipient_check?: boolean; + blinded_local?: boolean; }; }; produceBlindedBlock: {params: {slot: number}; query: {randao_reveal: string; graffiti: string}}; @@ -552,6 +556,7 @@ export function getReqSerializers(): ReqSerializers { skip_randao_verification: skipRandaoVerification, builder_selection: opts?.builderSelection, strict_fee_recipient_check: opts?.strictFeeRecipientCheck, + blinded_local: opts?.blindedLocal, }, }), parseReq: ({params, query}) => [ @@ -563,6 +568,7 @@ export function getReqSerializers(): ReqSerializers { feeRecipient: query.fee_recipient, builderSelection: query.builder_selection as BuilderSelection, strictFeeRecipientCheck: query.strict_fee_recipient_check, + blindedLocal: query.blinded_local, }, ], schema: { @@ -574,6 +580,7 @@ export function getReqSerializers(): ReqSerializers { skip_randao_verification: Schema.Boolean, builder_selection: Schema.String, strict_fee_recipient_check: Schema.Boolean, + blinded_local: Schema.Boolean, }, }, }; @@ -739,20 +746,32 @@ export function getReturnTypes(): ReturnTypes { if (data.executionPayloadBlinded) { return { execution_payload_blinded: true, + executionPayloadSource: data.executionPayloadSource, ...(produceBlindedBlock.toJson(data) as Record), }; } else { return { execution_payload_blinded: false, + executionPayloadSource: data.executionPayloadSource, ...(produceBlockOrContents.toJson(data) as Record), }; } }, fromJson: (data) => { - if ((data as {execution_payload_blinded: true}).execution_payload_blinded) { - return {executionPayloadBlinded: true, ...produceBlindedBlock.fromJson(data)}; + const executionPayloadBlinded = (data as {execution_payload_blinded: boolean}).execution_payload_blinded; + if (executionPayloadBlinded === undefined) { + throw Error(`Invalid executionPayloadBlinded=${executionPayloadBlinded} for fromJson deserialization`); + } + + // extract source from the data and assign defaults in the spec complaint manner if not present in response + const executionPayloadSource = + (data as {execution_payload_source: ProducedBlockSource}).execution_payload_source ?? + (executionPayloadBlinded ? ProducedBlockSource.builder : ProducedBlockSource.engine); + + if (executionPayloadBlinded) { + return {executionPayloadBlinded, executionPayloadSource, ...produceBlindedBlock.fromJson(data)}; } else { - return {executionPayloadBlinded: false, ...produceBlockOrContents.fromJson(data)}; + return {executionPayloadBlinded, executionPayloadSource, ...produceBlockOrContents.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 19350c4fe80e..8c666dd311f4 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -416,193 +416,204 @@ export function getValidatorApi({ } }; - const produceBlockV3: ServerApi["produceBlockV3"] = async function produceBlockV3( - slot, - randaoReveal, - graffiti, - // TODO deneb: skip randao verification - _skipRandaoVerification?: boolean, - {feeRecipient, builderSelection, strictFeeRecipientCheck}: routes.validator.ExtraProduceBlockOps = {} - ) { - notWhileSyncing(); - await waitForSlot(slot); // Must never request for a future slot > currentSlot - - // Process the queued attestations in the forkchoice for correct head estimation - // forkChoice.updateTime() might have already been called by the onSlot clock - // handler, in which case this should just return. - chain.forkChoice.updateTime(slot); - chain.recomputeForkChoiceHead(); - - const fork = config.getForkName(slot); - // set some sensible opts - builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit; - const isBuilderEnabled = - ForkSeq[fork] >= ForkSeq.bellatrix && - chain.executionBuilder !== undefined && - builderSelection !== routes.validator.BuilderSelection.ExecutionOnly; - - logger.verbose("Assembling block with produceBlockV3 ", { - fork, - builderSelection, + const produceEngineOrBuilderBlock: ServerApi["produceBlockV3"] = + async function produceEngineOrBuilderBlock( slot, - isBuilderEnabled, - strictFeeRecipientCheck, - }); - // 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 - produceBuilderBlindedBlock(slot, randaoReveal, graffiti, { - feeRecipient, - // skip checking and recomputing head in these individual produce calls - skipHeadChecksAndUpdate: true, - }).catch((e) => { - logger.error("produceBuilderBlindedBlock failed to produce block", {slot}, e); - return null; - }) - : null; + randaoReveal, + graffiti, + // TODO deneb: skip randao verification + _skipRandaoVerification?: boolean, + {feeRecipient, builderSelection, strictFeeRecipientCheck}: routes.validator.ExtraProduceBlockOps = {} + ) { + notWhileSyncing(); + await waitForSlot(slot); // Must never request for a future slot > currentSlot - 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 !== routes.validator.BuilderSelection.BuilderOnly - ? // TODO deneb: builderSelection needs to be figured out if to be done beacon side - // || builderSelection !== BuilderSelection.BuilderOnly - produceEngineFullBlockOrContents(slot, randaoReveal, graffiti, { + // Process the queued attestations in the forkchoice for correct head estimation + // forkChoice.updateTime() might have already been called by the onSlot clock + // handler, in which case this should just return. + chain.forkChoice.updateTime(slot); + chain.recomputeForkChoiceHead(); + + const fork = config.getForkName(slot); + // set some sensible opts + builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit; + const isBuilderEnabled = + ForkSeq[fork] >= ForkSeq.bellatrix && + chain.executionBuilder !== undefined && + builderSelection !== routes.validator.BuilderSelection.ExecutionOnly; + + logger.verbose("Assembling block with produceEngineOrBuilderBlock ", { + fork, + builderSelection, + slot, + isBuilderEnabled, + strictFeeRecipientCheck, + }); + // 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 + produceBuilderBlindedBlock(slot, randaoReveal, graffiti, { feeRecipient, - strictFeeRecipientCheck, // skip checking and recomputing head in these individual produce calls skipHeadChecksAndUpdate: true, }).catch((e) => { - logger.error("produceEngineFullBlockOrContents failed to produce block", {slot}, e); + logger.error("produceBuilderBlindedBlock failed to produce block", {slot}, e); return null; }) : 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< - routes.validator.ProduceBlockOrContentsRes | routes.validator.ProduceBlindedBlockRes | null - >( - [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]} : {}; - logger.verbose("Block production race (builder vs execution)", { - event, - ...eventRef, - delayMs, - cutoffMs: BLOCK_PRODUCTION_RACE_CUTOFF_MS, - timeoutMs: BLOCK_PRODUCTION_RACE_TIMEOUT_MS, - slot, - }); + 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 !== routes.validator.BuilderSelection.BuilderOnly + ? // TODO deneb: builderSelection needs to be figured out if to be done beacon side + // || builderSelection !== BuilderSelection.BuilderOnly + produceEngineFullBlockOrContents(slot, randaoReveal, graffiti, { + feeRecipient, + strictFeeRecipientCheck, + // skip checking and recomputing head in these individual produce calls + skipHeadChecksAndUpdate: true, + }).catch((e) => { + logger.error("produceEngineFullBlockOrContents failed to produce block", {slot}, e); + return null; + }) + : 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< + routes.validator.ProduceBlockOrContentsRes | routes.validator.ProduceBlindedBlockRes | null + >( + [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]} : {}; + logger.verbose("Block production race (builder vs execution)", { + event, + ...eventRef, + delayMs, + cutoffMs: BLOCK_PRODUCTION_RACE_CUTOFF_MS, + timeoutMs: BLOCK_PRODUCTION_RACE_TIMEOUT_MS, + slot, + }); + } + ); + if (blindedBlock instanceof Error) { + // error here means race cutoff exceeded + logger.error("Failed to produce builder block", {slot}, blindedBlock); + blindedBlock = null; } - ); - if (blindedBlock instanceof Error) { - // error here means race cutoff exceeded - logger.error("Failed to produce builder block", {slot}, blindedBlock); - blindedBlock = null; - } - if (fullBlock instanceof Error) { - logger.error("Failed to produce execution block", {slot}, fullBlock); + if (fullBlock instanceof Error) { + logger.error("Failed to produce execution block", {slot}, 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}` + ); } - } 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 consensusBlockValueBuilder = blindedBlock?.consensusBlockValue ?? BigInt(0); - const consensusBlockValueEngine = fullBlock?.consensusBlockValue ?? BigInt(0); + const builderPayloadValue = blindedBlock?.executionPayloadValue ?? BigInt(0); + const enginePayloadValue = fullBlock?.executionPayloadValue ?? BigInt(0); + const consensusBlockValueBuilder = blindedBlock?.consensusBlockValue ?? BigInt(0); + const consensusBlockValueEngine = fullBlock?.consensusBlockValue ?? BigInt(0); - const blockValueBuilder = builderPayloadValue + consensusBlockValueBuilder; - const blockValueEngine = enginePayloadValue + consensusBlockValueEngine; + const blockValueBuilder = builderPayloadValue + consensusBlockValueBuilder; + const blockValueEngine = enginePayloadValue + consensusBlockValueEngine; - let selectedSource: ProducedBlockSource | null = null; + let executionPayloadSource: ProducedBlockSource | null = null; - if (fullBlock && blindedBlock) { - switch (builderSelection) { - case routes.validator.BuilderSelection.MaxProfit: { - if (blockValueEngine >= blockValueBuilder) { - selectedSource = ProducedBlockSource.engine; - } else { - selectedSource = ProducedBlockSource.builder; + if (fullBlock && blindedBlock) { + switch (builderSelection) { + case routes.validator.BuilderSelection.MaxProfit: { + if (blockValueEngine >= blockValueBuilder) { + executionPayloadSource = ProducedBlockSource.engine; + } else { + executionPayloadSource = ProducedBlockSource.builder; + } + break; } - break; - } - case routes.validator.BuilderSelection.ExecutionOnly: { - selectedSource = ProducedBlockSource.engine; - break; - } + case routes.validator.BuilderSelection.ExecutionOnly: { + executionPayloadSource = ProducedBlockSource.engine; + break; + } - // For everything else just select the builder - default: { - selectedSource = ProducedBlockSource.builder; + // For everything else just select the builder + default: { + executionPayloadSource = ProducedBlockSource.builder; + } } + logger.verbose(`Selected executionPayloadSource=${executionPayloadSource} block`, { + builderSelection, + // winston logger doesn't like bigint + enginePayloadValue: `${enginePayloadValue}`, + builderPayloadValue: `${builderPayloadValue}`, + consensusBlockValueEngine: `${consensusBlockValueEngine}`, + consensusBlockValueBuilder: `${consensusBlockValueBuilder}`, + blockValueEngine: `${blockValueEngine}`, + blockValueBuilder: `${blockValueBuilder}`, + slot, + }); + } else if (fullBlock && !blindedBlock) { + executionPayloadSource = ProducedBlockSource.engine; + logger.verbose("Selected engine block: no builder block produced", { + // winston logger doesn't like bigint + enginePayloadValue: `${enginePayloadValue}`, + consensusBlockValueEngine: `${consensusBlockValueEngine}`, + blockValueEngine: `${blockValueEngine}`, + slot, + }); + } else if (blindedBlock && !fullBlock) { + executionPayloadSource = ProducedBlockSource.builder; + logger.verbose("Selected builder block: no engine block produced", { + // winston logger doesn't like bigint + builderPayloadValue: `${builderPayloadValue}`, + consensusBlockValueBuilder: `${consensusBlockValueBuilder}`, + blockValueBuilder: `${blockValueBuilder}`, + slot, + }); } - logger.verbose(`Selected ${selectedSource} block`, { - builderSelection, - // winston logger doesn't like bigint - enginePayloadValue: `${enginePayloadValue}`, - builderPayloadValue: `${builderPayloadValue}`, - consensusBlockValueEngine: `${consensusBlockValueEngine}`, - consensusBlockValueBuilder: `${consensusBlockValueBuilder}`, - blockValueEngine: `${blockValueEngine}`, - blockValueBuilder: `${blockValueBuilder}`, - slot, - }); - } else if (fullBlock && !blindedBlock) { - selectedSource = ProducedBlockSource.engine; - logger.verbose("Selected engine block: no builder block produced", { - // winston logger doesn't like bigint - enginePayloadValue: `${enginePayloadValue}`, - consensusBlockValueEngine: `${consensusBlockValueEngine}`, - blockValueEngine: `${blockValueEngine}`, - slot, - }); - } else if (blindedBlock && !fullBlock) { - selectedSource = ProducedBlockSource.builder; - logger.verbose("Selected builder block: no engine block produced", { - // winston logger doesn't like bigint - builderPayloadValue: `${builderPayloadValue}`, - consensusBlockValueBuilder: `${consensusBlockValueBuilder}`, - blockValueBuilder: `${blockValueBuilder}`, - slot, - }); - } - if (selectedSource === null) { - throw Error(`Failed to produce engine or builder block for slot=${slot}`); - } + if (executionPayloadSource === null) { + throw Error(`Failed to produce engine or builder block for slot=${slot}`); + } - if (selectedSource === ProducedBlockSource.engine) { - return {...fullBlock, executionPayloadBlinded: false} as routes.validator.ProduceBlockOrContentsRes & { - executionPayloadBlinded: false; - }; - } else { - return {...blindedBlock, executionPayloadBlinded: true} as routes.validator.ProduceBlindedBlockRes & { - executionPayloadBlinded: true; - }; - } - }; + if (executionPayloadSource === ProducedBlockSource.engine) { + return { + ...fullBlock, + executionPayloadBlinded: false, + executionPayloadSource, + } as routes.validator.ProduceBlockOrContentsRes & { + executionPayloadBlinded: false; + executionPayloadSource: ProducedBlockSource; + }; + } else { + return { + ...blindedBlock, + executionPayloadBlinded: true, + executionPayloadSource, + } as routes.validator.ProduceBlindedBlockRes & { + executionPayloadBlinded: true; + executionPayloadSource: ProducedBlockSource; + }; + } + }; const produceBlock: ServerApi["produceBlock"] = async function produceBlock( slot, @@ -621,7 +632,7 @@ export function getValidatorApi({ const produceEngineOrBuilderBlindedBlock: ServerApi["produceBlindedBlock"] = async function produceEngineOrBuilderBlindedBlock(slot, randaoReveal, graffiti) { - const {data, executionPayloadValue, consensusBlockValue, version} = await produceBlockV3( + const {data, executionPayloadValue, consensusBlockValue, version} = await produceEngineOrBuilderBlock( slot, randaoReveal, graffiti @@ -643,6 +654,55 @@ export function getValidatorApi({ } }; + const produceBlockV3: ServerApi["produceBlockV3"] = async function produceBlockV3( + slot, + randaoReveal, + graffiti, + skipRandaoVerification?: boolean, + opts: routes.validator.ExtraProduceBlockOps = {} + ) { + const produceBlockEngineOrBuilderRes = await produceEngineOrBuilderBlock( + slot, + randaoReveal, + graffiti, + skipRandaoVerification, + opts + ); + if (opts.blindedLocal === true) { + if (produceBlockEngineOrBuilderRes.executionPayloadBlinded) { + return produceBlockEngineOrBuilderRes; + } else { + if (isBlockContents(produceBlockEngineOrBuilderRes.data)) { + const {block} = produceBlockEngineOrBuilderRes.data; + const blindedBlock = beaconBlockToBlinded(config, block as allForks.AllForksExecution["BeaconBlock"]); + return { + ...produceBlockEngineOrBuilderRes, + data: blindedBlock, + executionPayloadBlinded: true, + } as routes.validator.ProduceBlindedBlockRes & { + executionPayloadBlinded: true; + executionPayloadSource: ProducedBlockSource; + }; + } else { + const blindedBlock = beaconBlockToBlinded( + config, + produceBlockEngineOrBuilderRes.data as allForks.AllForksExecution["BeaconBlock"] + ); + return { + ...produceBlockEngineOrBuilderRes, + data: blindedBlock, + executionPayloadBlinded: true, + } as routes.validator.ProduceBlindedBlockRes & { + executionPayloadBlinded: true; + executionPayloadSource: ProducedBlockSource; + }; + } + } + } else { + return produceBlockEngineOrBuilderRes; + } + }; + return { produceBlock, produceBlockV2: produceEngineFullBlockOrContents, diff --git a/packages/cli/src/cmds/validator/handler.ts b/packages/cli/src/cmds/validator/handler.ts index fe14cedbcca1..6c66ab51fa3f 100644 --- a/packages/cli/src/cmds/validator/handler.ts +++ b/packages/cli/src/cmds/validator/handler.ts @@ -170,6 +170,7 @@ export async function validatorHandler(args: IValidatorCliArgs & GlobalArgs): Pr distributed: args.distributed, useProduceBlockV3: args.useProduceBlockV3, broadcastValidation: parseBroadcastValidation(args.broadcastValidation), + blindedLocal: args.blindedLocal }, metrics ); diff --git a/packages/cli/src/cmds/validator/options.ts b/packages/cli/src/cmds/validator/options.ts index 4f0ec476f01c..41069cfbdd34 100644 --- a/packages/cli/src/cmds/validator/options.ts +++ b/packages/cli/src/cmds/validator/options.ts @@ -48,6 +48,7 @@ export type IValidatorCliArgs = AccountValidatorArgs & useProduceBlockV3?: boolean; broadcastValidation?: string; + blindedLocal?: boolean; importKeystores?: string[]; importKeystoresPassword?: string; @@ -257,6 +258,12 @@ export const validatorOptions: CliCommandOptions = { defaultDescription: `${defaultOptions.broadcastValidation}`, }, + blindedLocal: { + type: "string", + description: "Request fetching local block in blinded format for produceBlockV3", + defaultDescription: `${defaultOptions.blindedLocal}`, + }, + importKeystores: { alias: ["keystore"], // Backwards compatibility with old `validator import` cmdx description: "Path(s) to a directory or single file path to validator keystores, i.e. Launchpad validators", diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index e30ac2b96b6e..0796621c0664 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -34,6 +34,7 @@ type FullOrBlindedBlockWithContents = block: allForks.BeaconBlock; contents: null; executionPayloadBlinded: false; + executionPayloadSource: ProducedBlockSource.engine; } | { version: ForkBlobs; @@ -43,15 +44,22 @@ type FullOrBlindedBlockWithContents = blobs: deneb.Blobs; }; executionPayloadBlinded: false; + executionPayloadSource: ProducedBlockSource.engine; } | { version: ForkExecution; block: allForks.BlindedBeaconBlock; contents: null; executionPayloadBlinded: true; + executionPayloadSource: ProducedBlockSource; }; type DebugLogCtx = {debugLogCtx: Record}; +type BlockProposalOpts = { + useProduceBlockV3: boolean; + broadcastValidation: routes.beacon.BroadcastValidation; + blindedLocal: boolean; +}; /** * Service that sets up and handles validator block proposal duties. */ @@ -65,7 +73,7 @@ export class BlockProposingService { private readonly clock: IClock, private readonly validatorStore: ValidatorStore, private readonly metrics: Metrics | null, - private readonly opts: {useProduceBlockV3: boolean; broadcastValidation: routes.beacon.BroadcastValidation} + private readonly opts: BlockProposalOpts ) { this.dutiesService = new BlockDutiesService( config, @@ -116,6 +124,7 @@ export class BlockProposingService { const strictFeeRecipientCheck = this.validatorStore.strictFeeRecipientCheck(pubkeyHex); const builderSelection = this.validatorStore.getBuilderSelection(pubkeyHex); const feeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex); + const blindedLocal = this.opts.blindedLocal; this.logger.debug("Producing block", { ...debugLogCtx, @@ -123,6 +132,7 @@ export class BlockProposingService { feeRecipient, strictFeeRecipientCheck, useProduceBlockV3: this.opts.useProduceBlockV3, + blindedLocal, }); this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot)); @@ -131,6 +141,7 @@ export class BlockProposingService { feeRecipient, strictFeeRecipientCheck, builderSelection, + blindedLocal, }).catch((e: Error) => { this.metrics?.blockProposingErrors.inc({error: "produce"}); throw extendError(e, "Failed to produce block"); @@ -225,14 +236,23 @@ export class BlockProposingService { const res = await this.api.validator.produceBlockV2(slot, randaoReveal, graffiti); ApiError.assert(res, "Failed to produce block: validator.produceBlockV2"); const {response} = res; - return parseProduceBlockResponse({executionPayloadBlinded: false, ...response}, debugLogCtx); + const executionPayloadSource = ProducedBlockSource.engine; + + return parseProduceBlockResponse( + {executionPayloadBlinded: false, executionPayloadSource, ...response}, + debugLogCtx + ); } else { Object.assign(debugLogCtx, {api: "produceBlindedBlock"}); const res = await this.api.validator.produceBlindedBlock(slot, randaoReveal, graffiti); ApiError.assert(res, "Failed to produce block: validator.produceBlockV2"); const {response} = res; + const executionPayloadSource = ProducedBlockSource.builder; - return parseProduceBlockResponse({executionPayloadBlinded: true, ...response}, debugLogCtx); + return parseProduceBlockResponse( + {executionPayloadBlinded: true, executionPayloadSource, ...response}, + debugLogCtx + ); } }; } @@ -247,6 +267,7 @@ function parseProduceBlockResponse( contents: null, version: response.version, executionPayloadBlinded: true, + executionPayloadSource: response.executionPayloadSource, debugLogCtx, } as FullOrBlindedBlockWithContents & DebugLogCtx; } else { @@ -256,6 +277,7 @@ function parseProduceBlockResponse( contents: {blobs: response.data.blobs, kzgProofs: response.data.kzgProofs}, version: response.version, executionPayloadBlinded: false, + executionPayloadSource: response.executionPayloadSource, debugLogCtx, } as FullOrBlindedBlockWithContents & DebugLogCtx; } else { @@ -264,6 +286,7 @@ function parseProduceBlockResponse( contents: null, version: response.version, executionPayloadBlinded: false, + executionPayloadSource: response.executionPayloadSource, debugLogCtx, } as FullOrBlindedBlockWithContents & DebugLogCtx; } diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 698de1dc7053..8cafaa5b14b6 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -127,6 +127,8 @@ export const defaultOptions = { useProduceBlockV3: false, // spec asks for gossip validation by default broadcastValidation: routes.beacon.BroadcastValidation.gossip, + // should request fetching the locally produced block in blinded format + blindedLocal: false, }; /** diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index 571aec71e019..f28a9afbaff6 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -58,6 +58,7 @@ export type ValidatorOptions = { distributed?: boolean; useProduceBlockV3?: boolean; broadcastValidation?: routes.beacon.BroadcastValidation; + blindedLocal?: boolean; }; // TODO: Extend the timeout, and let it be customizable @@ -210,6 +211,7 @@ export class Validator { const blockProposingService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, metrics, { useProduceBlockV3: opts.useProduceBlockV3 ?? defaultOptions.useProduceBlockV3, broadcastValidation: opts.broadcastValidation ?? defaultOptions.broadcastValidation, + blindedLocal: opts.blindedLocal ?? defaultOptions.blindedLocal, }); const attestationService = new AttestationService(