From 383859c2d3e590de71f747848fed05f6744c0150 Mon Sep 17 00:00:00 2001 From: harkamal Date: Mon, 25 Dec 2023 15:29:10 +0530 Subject: [PATCH] feat: add and support builder_boost_factor query param to produceBlockV3 api --- packages/api/src/beacon/routes/validator.ts | 8 +++ .../src/api/impl/validator/index.ts | 14 ++++- packages/cli/src/cmds/validator/handler.ts | 1 + packages/cli/src/cmds/validator/options.ts | 8 +++ packages/validator/src/services/block.ts | 60 +++++++++++++------ .../src/services/prepareBeaconProposer.ts | 3 +- .../validator/src/services/validatorStore.ts | 29 ++++++++- 7 files changed, 101 insertions(+), 22 deletions(-) diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index cedc0deff52c..3e4fa6251f82 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -53,6 +53,10 @@ export enum BuilderSelection { export type ExtraProduceBlockOps = { feeRecipient?: string; builderSelection?: BuilderSelection; + // precise value isn't required because super high values will be treated as always builder prefered + // and hence UintNum64 is sufficient. If this param is present, builderSelection will be infered to + // be of maxprofit (unless explicity provided) with this %age boost factor applied to the builder values + builderBoostFactor?: UintNum64; strictFeeRecipientCheck?: boolean; blindedLocal?: boolean; }; @@ -487,6 +491,7 @@ export type ReqTypes = { skip_randao_verification?: boolean; fee_recipient?: string; builder_selection?: string; + builder_boost_factor?: UintNum64; strict_fee_recipient_check?: boolean; blinded_local?: boolean; }; @@ -555,6 +560,7 @@ export function getReqSerializers(): ReqSerializers { fee_recipient: opts?.feeRecipient, skip_randao_verification: skipRandaoVerification, builder_selection: opts?.builderSelection, + builder_boost_factor: opts?.builderBoostFactor, strict_fee_recipient_check: opts?.strictFeeRecipientCheck, blinded_local: opts?.blindedLocal, }, @@ -567,6 +573,7 @@ export function getReqSerializers(): ReqSerializers { { feeRecipient: query.fee_recipient, builderSelection: query.builder_selection as BuilderSelection, + builderBoostFactor: query.builder_boost_factor, strictFeeRecipientCheck: query.strict_fee_recipient_check, blindedLocal: query.blinded_local, }, @@ -579,6 +586,7 @@ export function getReqSerializers(): ReqSerializers { fee_recipient: Schema.String, skip_randao_verification: Schema.Boolean, builder_selection: Schema.String, + builder_boost_factor: Schema.Uint, strict_fee_recipient_check: Schema.Boolean, blinded_local: Schema.Boolean, }, diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index a1f59d62911b..5e944ce3e383 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -423,7 +423,12 @@ export function getValidatorApi({ graffiti, // TODO deneb: skip randao verification _skipRandaoVerification?: boolean, - {feeRecipient, builderSelection, strictFeeRecipientCheck}: routes.validator.ExtraProduceBlockOps = {} + { + feeRecipient, + builderSelection, + builderBoostFactor, + strictFeeRecipientCheck, + }: routes.validator.ExtraProduceBlockOps = {} ) { notWhileSyncing(); await waitForSlot(slot); // Must never request for a future slot > currentSlot @@ -436,7 +441,10 @@ export function getValidatorApi({ const fork = config.getForkName(slot); // set some sensible opts + // builderSelection will be deprecated and will run in mode MaxProfit if builder is enabled + // and the actual selection will be determined using builderBoostFactor passed by the validator builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit; + builderBoostFactor = builderBoostFactor ?? 100; const isBuilderEnabled = ForkSeq[fork] >= ForkSeq.bellatrix && chain.executionBuilder !== undefined && @@ -448,6 +456,7 @@ export function getValidatorApi({ slot, isBuilderEnabled, strictFeeRecipientCheck, + builderBoostFactor, }); // Start calls for building execution and builder blocks const blindedBlockPromise = isBuilderEnabled @@ -541,7 +550,7 @@ export function getValidatorApi({ if (fullBlock && blindedBlock) { switch (builderSelection) { case routes.validator.BuilderSelection.MaxProfit: { - if (blockValueEngine >= blockValueBuilder) { + if (blockValueEngine >= (blockValueBuilder * BigInt(builderBoostFactor)) / BigInt(100)) { executionPayloadSource = ProducedBlockSource.engine; } else { executionPayloadSource = ProducedBlockSource.builder; @@ -561,6 +570,7 @@ export function getValidatorApi({ } logger.verbose(`Selected executionPayloadSource=${executionPayloadSource} block`, { builderSelection, + builderBoostFactor, // winston logger doesn't like bigint enginePayloadValue: `${enginePayloadValue}`, builderPayloadValue: `${builderPayloadValue}`, diff --git a/packages/cli/src/cmds/validator/handler.ts b/packages/cli/src/cmds/validator/handler.ts index 7713bf5a6dd1..753a38e9b65d 100644 --- a/packages/cli/src/cmds/validator/handler.ts +++ b/packages/cli/src/cmds/validator/handler.ts @@ -227,6 +227,7 @@ function getProposerConfigFromArgs( selection: parseBuilderSelection( args["builder.selection"] ?? (args["builder"] ? defaultOptions.builderAliasSelection : undefined) ), + boostFactor: args["builder.boostFactor"] }, }; diff --git a/packages/cli/src/cmds/validator/options.ts b/packages/cli/src/cmds/validator/options.ts index 41069cfbdd34..2714f912e266 100644 --- a/packages/cli/src/cmds/validator/options.ts +++ b/packages/cli/src/cmds/validator/options.ts @@ -45,6 +45,7 @@ export type IValidatorCliArgs = AccountValidatorArgs & builder?: boolean; "builder.selection"?: string; + "builder.boostFactor"?: number, useProduceBlockV3?: boolean; broadcastValidation?: string; @@ -246,6 +247,13 @@ export const validatorOptions: CliCommandOptions = { group: "builder", }, + "builder.boostFactor": { + type: "number", + description: "A factor in percentage requested to block producing beacon to boost (>100) or dampen(<100) builder block value for selection against engine, is overriden when `--builder.selection` set to anything other than `maxprofit`", + defaultDescription: `${defaultOptions.builderBoostFactor}`, + group: "builder", + }, + useProduceBlockV3: { type: "boolean", description: "Enable/disable usage of produceBlockV3 that might not be supported by all beacon clients yet", diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index ec3b0039ec03..ffdffeaa7cf8 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -121,13 +121,15 @@ export class BlockProposingService { const debugLogCtx = {...logCtx, validator: pubkeyHex}; const strictFeeRecipientCheck = this.validatorStore.strictFeeRecipientCheck(pubkeyHex); - const builderSelection = this.validatorStore.getBuilderSelection(pubkeyHex); + const {selection: builderSelection, boostFactor: builderBoostFactor} = + this.validatorStore.getBuilderSelectionParams(pubkeyHex); const feeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex); const blindedLocal = this.opts.blindedLocal; this.logger.debug("Producing block", { ...debugLogCtx, builderSelection, + builderBoostFactor, feeRecipient, strictFeeRecipientCheck, useProduceBlockV3: this.opts.useProduceBlockV3, @@ -139,15 +141,20 @@ export class BlockProposingService { const produceOpts = { feeRecipient, strictFeeRecipientCheck, - builderSelection, + builderBoostFactor, blindedLocal, }; - const blockContents = await produceBlockFn(this.config, slot, randaoReveal, graffiti, produceOpts).catch( - (e: Error) => { - this.metrics?.blockProposingErrors.inc({error: "produce"}); - throw extendError(e, "Failed to produce block"); - } - ); + const blockContents = await produceBlockFn( + this.config, + slot, + randaoReveal, + graffiti, + produceOpts, + builderSelection + ).catch((e: Error) => { + this.metrics?.blockProposingErrors.inc({error: "produce"}); + throw extendError(e, "Failed to produce block"); + }); this.logger.debug("Produced block", {...debugLogCtx, ...blockContents.debugLogCtx}); this.metrics?.blocksProduced.inc(); @@ -195,12 +202,14 @@ export class BlockProposingService { slot: Slot, randaoReveal: BLSSignature, graffiti: string, - {feeRecipient, strictFeeRecipientCheck, builderSelection}: routes.validator.ExtraProduceBlockOps + {feeRecipient, strictFeeRecipientCheck, builderBoostFactor}: routes.validator.ExtraProduceBlockOps, + builderSelection: routes.validator.BuilderSelection ): Promise => { const res = await this.api.validator.produceBlockV3(slot, randaoReveal, graffiti, false, { feeRecipient, builderSelection, strictFeeRecipientCheck, + builderBoostFactor, }); ApiError.assert(res, "Failed to produce block: validator.produceBlockV2"); const {response} = res; @@ -221,7 +230,7 @@ export class BlockProposingService { api: "produceBlockV3", }; - return parseProduceBlockResponse(response, debugLogCtx); + return parseProduceBlockResponse(response, debugLogCtx, builderSelection); }; /** a wrapper function used for backward compatibility with the clients who don't have v3 implemented yet */ @@ -230,7 +239,8 @@ export class BlockProposingService { slot: Slot, randaoReveal: BLSSignature, graffiti: string, - {builderSelection}: routes.validator.ExtraProduceBlockOps + _opts: routes.validator.ExtraProduceBlockOps, + builderSelection: routes.validator.BuilderSelection ): Promise => { // other clients have always implemented builder vs execution race in produce blinded block // so if builderSelection is executiononly then only we call produceBlockV2 else produceBlockV3 always @@ -246,7 +256,8 @@ export class BlockProposingService { return parseProduceBlockResponse( {executionPayloadBlinded: false, executionPayloadSource, ...response}, - debugLogCtx + debugLogCtx, + builderSelection ); } else { Object.assign(debugLogCtx, {api: "produceBlindedBlock"}); @@ -257,7 +268,8 @@ export class BlockProposingService { return parseProduceBlockResponse( {executionPayloadBlinded: true, executionPayloadSource, ...response}, - debugLogCtx + debugLogCtx, + builderSelection ); } }; @@ -265,15 +277,29 @@ export class BlockProposingService { function parseProduceBlockResponse( response: routes.validator.ProduceFullOrBlindedBlockOrContentsRes, - debugLogCtx: Record + debugLogCtx: Record, + builderSelection: routes.validator.BuilderSelection ): FullOrBlindedBlockWithContents & DebugLogCtx { + const executionPayloadSource = response.executionPayloadSource; + + if ( + (builderSelection === routes.validator.BuilderSelection.BuilderOnly && + executionPayloadSource === ProducedBlockSource.engine) || + (builderSelection === routes.validator.BuilderSelection.ExecutionOnly && + executionPayloadSource === ProducedBlockSource.builder) + ) { + throw Error( + `Block not produced as per desired builderSelection=${builderSelection} executionPayloadSource=${executionPayloadSource}` + ); + } + if (response.executionPayloadBlinded) { return { block: response.data, contents: null, version: response.version, executionPayloadBlinded: true, - executionPayloadSource: response.executionPayloadSource, + executionPayloadSource, debugLogCtx, } as FullOrBlindedBlockWithContents & DebugLogCtx; } else { @@ -283,7 +309,7 @@ function parseProduceBlockResponse( contents: {blobs: response.data.blobs, kzgProofs: response.data.kzgProofs}, version: response.version, executionPayloadBlinded: false, - executionPayloadSource: response.executionPayloadSource, + executionPayloadSource, debugLogCtx, } as FullOrBlindedBlockWithContents & DebugLogCtx; } else { @@ -292,7 +318,7 @@ function parseProduceBlockResponse( contents: null, version: response.version, executionPayloadBlinded: false, - executionPayloadSource: response.executionPayloadSource, + executionPayloadSource, debugLogCtx, } as FullOrBlindedBlockWithContents & DebugLogCtx; } diff --git a/packages/validator/src/services/prepareBeaconProposer.ts b/packages/validator/src/services/prepareBeaconProposer.ts index 7ca939fb0c41..7d7907a4592d 100644 --- a/packages/validator/src/services/prepareBeaconProposer.ts +++ b/packages/validator/src/services/prepareBeaconProposer.ts @@ -86,7 +86,8 @@ export function pollBuilderValidatorRegistration( .filter( (pubkeyHex): pubkeyHex is string => pubkeyHex !== undefined && - validatorStore.getBuilderSelection(pubkeyHex) !== routes.validator.BuilderSelection.ExecutionOnly + validatorStore.getBuilderSelectionParams(pubkeyHex).selection !== + routes.validator.BuilderSelection.ExecutionOnly ); if (pubkeyHexes.length > 0) { diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 8cafaa5b14b6..61c794414303 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -69,6 +69,7 @@ type DefaultProposerConfig = { builder: { gasLimit: number; selection: routes.validator.BuilderSelection; + boostFactor: number; }; }; @@ -79,6 +80,7 @@ export type ProposerConfig = { builder?: { gasLimit?: number; selection?: routes.validator.BuilderSelection; + boostFactor?: number; }; }; @@ -123,6 +125,7 @@ export const defaultOptions = { defaultGasLimit: 30_000_000, builderSelection: routes.validator.BuilderSelection.ExecutionOnly, builderAliasSelection: routes.validator.BuilderSelection.MaxProfit, + builderBoostFactor: 100, // turn it off by default, turn it back on once other clients support v3 api useProduceBlockV3: false, // spec asks for gossip validation by default @@ -131,6 +134,8 @@ export const defaultOptions = { blindedLocal: false, }; +const MAX_BUILDER_BOOST_FACTOR = 2 ** 64 - 1; + /** * Service that sets up and handles validator attester duties. */ @@ -162,6 +167,7 @@ export class ValidatorStore { builder: { gasLimit: defaultConfig.builder?.gasLimit ?? defaultOptions.defaultGasLimit, selection: defaultConfig.builder?.selection ?? defaultOptions.builderSelection, + boostFactor: defaultConfig.builder?.boostFactor ?? defaultOptions.builderBoostFactor, }, }; @@ -252,8 +258,27 @@ export class ValidatorStore { delete validatorData["graffiti"]; } - getBuilderSelection(pubkeyHex: PubkeyHex): routes.validator.BuilderSelection { - return (this.validators.get(pubkeyHex)?.builder || {}).selection ?? this.defaultProposerConfig.builder.selection; + getBuilderSelectionParams(pubkeyHex: PubkeyHex): {selection: routes.validator.BuilderSelection; boostFactor: number} { + const selection = + (this.validators.get(pubkeyHex)?.builder || {}).selection ?? this.defaultProposerConfig.builder.selection; + + let boostFactor; + switch (selection) { + case routes.validator.BuilderSelection.MaxProfit: + boostFactor = + (this.validators.get(pubkeyHex)?.builder || {}).boostFactor ?? this.defaultProposerConfig.builder.boostFactor; + break; + + case routes.validator.BuilderSelection.BuilderAlways: + case routes.validator.BuilderSelection.BuilderOnly: + boostFactor = MAX_BUILDER_BOOST_FACTOR; + break; + + case routes.validator.BuilderSelection.ExecutionOnly: + boostFactor = 0; + } + + return {selection, boostFactor}; } strictFeeRecipientCheck(pubkeyHex: PubkeyHex): boolean {