diff --git a/.wordlist.txt b/.wordlist.txt index 9b82a3a78396..83d2bd51aa73 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -109,6 +109,7 @@ nodemodule overriden params plaintext +produceBlockV prover req reqresp diff --git a/docs/usage/beacon-management.md b/docs/usage/beacon-management.md index 7295db64098d..46b6f2e456c8 100644 --- a/docs/usage/beacon-management.md +++ b/docs/usage/beacon-management.md @@ -101,7 +101,7 @@ A young testnet should take a few hours to sync. If you see multiple or consiste ### Checkpoint Sync -If you are starting your node from a blank db, like starting from genesis, or from the last saved state in db and the network is now far ahead, your node will be susceptible to "long range attacks." Ethereum's solution to this is via something called weak subjectivity. [Read Vitalik's illuminating post explaining weak subjectivity.](https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity/). +If you are starting your node from a blank db, like starting from genesis, or from the last saved state in db and the network is now far ahead, your node will be susceptible to "long range attacks." Ethereum's solution to this is via something called weak subjectivity. [Read Vitalik's illuminating post explaining weak subjectivity.](https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity/). If you have a synced beacon node available (e.g., your friend's node or an infrastructure provider) and a trusted checkpoint you can rely on, you can start off your beacon node in under a minute! And at the same time kicking the "long range attack" in its butt! diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index 417d04044649..278637d53a2c 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -1,7 +1,17 @@ import {ContainerType} from "@chainsafe/ssz"; import {ForkName} from "@lodestar/params"; import {ChainForkConfig} from "@lodestar/config"; -import {phase0, allForks, Slot, Root, ssz, RootHex, deneb} from "@lodestar/types"; +import { + phase0, + allForks, + Slot, + Root, + ssz, + RootHex, + deneb, + isSignedBlockContents, + isSignedBlindedBlockContents, +} from "@lodestar/types"; import { RoutesData, @@ -21,12 +31,8 @@ import {HttpStatusCode} from "../../../utils/client/httpStatusCode.js"; import {parseAcceptHeader, writeAcceptHeader} from "../../../utils/acceptHeader.js"; import {ApiClientResponse, ResponseFormat} from "../../../interfaces.js"; import { - SignedBlockContents, - SignedBlindedBlockContents, - isSignedBlockContents, - isSignedBlindedBlockContents, - AllForksSignedBlockContentsReqSerializer, - AllForksSignedBlindedBlockContentsReqSerializer, + allForksSignedBlockContentsReqSerializer, + allForksSignedBlindedBlockContentsReqSerializer, } from "../../../utils/routes.js"; // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes @@ -175,7 +181,7 @@ export type Api = { * @param requestBody The `SignedBeaconBlock` object composed of `BeaconBlock` object (produced by beacon node) and validator signature. * @returns any The block was validated successfully and has been broadcast. It has also been integrated into the beacon node's database. */ - publishBlock(blockOrContents: allForks.SignedBeaconBlock | SignedBlockContents): Promise< + publishBlock(blockOrContents: allForks.SignedBeaconBlockOrContents): Promise< ApiClientResponse< { [HttpStatusCode.OK]: void; @@ -186,7 +192,7 @@ export type Api = { >; publishBlockV2( - blockOrContents: allForks.SignedBeaconBlock | SignedBlockContents, + blockOrContents: allForks.SignedBeaconBlockOrContents, opts: {broadcastValidation?: BroadcastValidation} ): Promise< ApiClientResponse< @@ -202,7 +208,7 @@ export type Api = { * Publish a signed blinded block by submitting it to the mev relay and patching in the block * transactions beacon node gets in response. */ - publishBlindedBlock(blindedBlockOrContents: allForks.SignedBlindedBeaconBlock | SignedBlindedBlockContents): Promise< + publishBlindedBlock(blindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents): Promise< ApiClientResponse< { [HttpStatusCode.OK]: void; @@ -213,7 +219,7 @@ export type Api = { >; publishBlindedBlockV2( - blindedBlockOrContents: allForks.SignedBlindedBeaconBlock | SignedBlindedBlockContents, + blindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents, opts: {broadcastValidation?: BroadcastValidation} ): Promise< ApiClientResponse< @@ -293,15 +299,15 @@ export function getReqSerializers(config: ChainForkConfig): ReqSerializers config.getForkTypes(data.message.slot).SignedBeaconBlock; - const AllForksSignedBlockOrContents: TypeJson = { + const AllForksSignedBlockOrContents: TypeJson = { toJson: (data) => isSignedBlockContents(data) - ? AllForksSignedBlockContentsReqSerializer(getSignedBeaconBlockType).toJson(data) + ? allForksSignedBlockContentsReqSerializer(getSignedBeaconBlockType).toJson(data) : getSignedBeaconBlockType(data).toJson(data), fromJson: (data) => (data as {signed_block: unknown}).signed_block !== undefined - ? AllForksSignedBlockContentsReqSerializer(getSignedBeaconBlockType).fromJson(data) + ? allForksSignedBlockContentsReqSerializer(getSignedBeaconBlockType).fromJson(data) : getSignedBeaconBlockType(data as allForks.SignedBeaconBlock).fromJson(data), }; @@ -310,18 +316,17 @@ export function getReqSerializers(config: ChainForkConfig): ReqSerializers config.getBlindedForkTypes(data.message.slot).SignedBeaconBlock; - const AllForksSignedBlindedBlockOrContents: TypeJson = - { - toJson: (data) => - isSignedBlindedBlockContents(data) - ? AllForksSignedBlindedBlockContentsReqSerializer(getSignedBlindedBeaconBlockType).toJson(data) - : getSignedBlindedBeaconBlockType(data).toJson(data), - - fromJson: (data) => - (data as {signed_blinded_block: unknown}).signed_blinded_block !== undefined - ? AllForksSignedBlindedBlockContentsReqSerializer(getSignedBlindedBeaconBlockType).fromJson(data) - : getSignedBlindedBeaconBlockType(data as allForks.SignedBlindedBeaconBlock).fromJson(data), - }; + const AllForksSignedBlindedBlockOrContents: TypeJson = { + toJson: (data) => + isSignedBlindedBlockContents(data) + ? allForksSignedBlindedBlockContentsReqSerializer(getSignedBlindedBeaconBlockType).toJson(data) + : getSignedBlindedBeaconBlockType(data).toJson(data), + + fromJson: (data) => + (data as {signed_blinded_block: unknown}).signed_blinded_block !== undefined + ? allForksSignedBlindedBlockContentsReqSerializer(getSignedBlindedBeaconBlockType).fromJson(data) + : getSignedBlindedBeaconBlockType(data as allForks.SignedBlindedBeaconBlock).fromJson(data), + }; return { getBlock: getBlockReq, diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 3b02946f29ef..d7c063d38ede 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -1,5 +1,5 @@ import {ContainerType, fromHexString, toHexString, Type} from "@chainsafe/ssz"; -import {ForkName, isForkBlobs, isForkExecution} from "@lodestar/params"; +import {ForkName, ForkBlobs, isForkBlobs, isForkExecution, ForkPreBlobs} from "@lodestar/params"; import { allForks, altair, @@ -27,22 +27,46 @@ import { ArrayOf, Schema, WithVersion, - WithBlockValue, + WithExecutionPayloadValue, reqOnlyBody, ReqSerializers, jsonType, ContainerDataExecutionOptimistic, ContainerData, + TypeJson, } from "../../utils/index.js"; import {fromU64Str, fromGraffitiHex, toU64Str, U64Str, toGraffitiHex} from "../../utils/serdes.js"; -import { - BlockContents, - BlindedBlockContents, - AllForksBlockContentsResSerializer, - AllForksBlindedBlockContentsResSerializer, -} from "../../utils/routes.js"; +import {allForksBlockContentsResSerializer, allForksBlindedBlockContentsResSerializer} from "../../utils/routes.js"; import {ExecutionOptimistic} from "./beacon/block.js"; +export enum BuilderSelection { + BuilderAlways = "builderalways", + MaxProfit = "maxprofit", + /** Only activate builder flow for DVT block proposal protocols */ + BuilderOnly = "builderonly", + /** Only builds execution block*/ + ExecutionOnly = "executiononly", +} + +export type ExtraProduceBlockOps = { + feeRecipient?: string; + builderSelection?: BuilderSelection; + strictFeeRecipientCheck?: boolean; +}; + +export type ProduceBlockOrContentsRes = {executionPayloadValue: Wei} & ( + | {data: allForks.BeaconBlock; version: ForkPreBlobs} + | {data: allForks.BlockContents; version: ForkBlobs} +); +export type ProduceBlindedBlockOrContentsRes = {executionPayloadValue: Wei} & ( + | {data: allForks.BlindedBeaconBlock; version: ForkPreBlobs} + | {data: allForks.BlindedBlockContents; version: ForkBlobs} +); + +export type ProduceFullOrBlindedBlockOrContentsRes = + | (ProduceBlockOrContentsRes & {executionPayloadBlinded: false}) + | (ProduceBlindedBlockOrContentsRes & {executionPayloadBlinded: true}); + // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes export type BeaconCommitteeSubscription = { @@ -201,11 +225,10 @@ export type Api = { produceBlock( slot: Slot, randaoReveal: BLSSignature, - graffiti: string, - feeRecipient?: string + graffiti: string ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: allForks.BeaconBlock; blockValue: Wei}}, + {[HttpStatusCode.OK]: {data: allForks.BeaconBlock}}, HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE > >; @@ -221,13 +244,37 @@ export type Api = { * @throws ApiError */ produceBlockV2( + slot: Slot, + randaoReveal: BLSSignature, + graffiti: string + ): Promise< + ApiClientResponse< + {[HttpStatusCode.OK]: ProduceBlockOrContentsRes}, + HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE + > + >; + + /** + * Requests a beacon node to produce a valid block, which can then be signed by a validator. + * Metadata in the response indicates the type of block produced, and the supported types of block + * will be added to as forks progress. + * @param slot The slot for which the block should be proposed. + * @param randaoReveal The validator's randao reveal value. + * @param graffiti Arbitrary data validator wants to include in block. + * @returns any Success response + * @throws ApiError + */ + produceBlockV3( slot: Slot, randaoReveal: BLSSignature, graffiti: string, - feeRecipient?: string + skipRandaoVerification?: boolean, + opts?: ExtraProduceBlockOps ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: allForks.BeaconBlock | BlockContents; version: ForkName; blockValue: Wei}}, + { + [HttpStatusCode.OK]: ProduceFullOrBlindedBlockOrContentsRes; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE > >; @@ -235,16 +282,11 @@ export type Api = { produceBlindedBlock( slot: Slot, randaoReveal: BLSSignature, - graffiti: string, - feeRecipient?: string + graffiti: string ): Promise< ApiClientResponse< { - [HttpStatusCode.OK]: { - data: allForks.BlindedBeaconBlock | BlindedBlockContents; - version: ForkName; - blockValue: Wei; - }; + [HttpStatusCode.OK]: ProduceBlindedBlockOrContentsRes; }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE > @@ -410,6 +452,7 @@ export const routesData: RoutesData = { getSyncCommitteeDuties: {url: "/eth/v1/validator/duties/sync/{epoch}", method: "POST"}, produceBlock: {url: "/eth/v1/validator/blocks/{slot}", method: "GET"}, produceBlockV2: {url: "/eth/v2/validator/blocks/{slot}", method: "GET"}, + produceBlockV3: {url: "/eth/v3/validator/blocks/{slot}", method: "GET"}, produceBlindedBlock: {url: "/eth/v1/validator/blinded_blocks/{slot}", method: "GET"}, produceAttestationData: {url: "/eth/v1/validator/attestation_data", method: "GET"}, produceSyncCommitteeContribution: {url: "/eth/v1/validator/sync_committee_contribution", method: "GET"}, @@ -432,6 +475,17 @@ export type ReqTypes = { getSyncCommitteeDuties: {params: {epoch: Epoch}; body: U64Str[]}; produceBlock: {params: {slot: number}; query: {randao_reveal: string; graffiti: string}}; produceBlockV2: {params: {slot: number}; query: {randao_reveal: string; graffiti: string; fee_recipient?: string}}; + produceBlockV3: { + params: {slot: number}; + query: { + randao_reveal: string; + graffiti: string; + skip_randao_verification?: boolean; + fee_recipient?: string; + builder_selection?: string; + strict_fee_recipient_check?: boolean; + }; + }; produceBlindedBlock: {params: {slot: number}; query: {randao_reveal: string; graffiti: string}}; produceAttestationData: {query: {slot: number; committee_index: number}}; produceSyncCommitteeContribution: {query: {slot: number; subcommittee_index: number; beacon_block_root: string}}; @@ -487,20 +541,39 @@ export function getReqSerializers(): ReqSerializers { {jsonCase: "eth2"} ); - const produceBlock: ReqSerializers["produceBlockV2"] = { - writeReq: (slot, randaoReveal, graffiti, feeRecipient) => ({ + const produceBlockV3: ReqSerializers["produceBlockV3"] = { + writeReq: (slot, randaoReveal, graffiti, skipRandaoVerification, opts) => ({ params: {slot}, - query: {randao_reveal: toHexString(randaoReveal), graffiti: toGraffitiHex(graffiti), fee_recipient: feeRecipient}, + query: { + randao_reveal: toHexString(randaoReveal), + graffiti: toGraffitiHex(graffiti), + fee_recipient: opts?.feeRecipient, + skip_randao_verification: skipRandaoVerification, + builder_selection: opts?.builderSelection, + strict_fee_recipient_check: opts?.strictFeeRecipientCheck, + }, }), parseReq: ({params, query}) => [ params.slot, fromHexString(query.randao_reveal), fromGraffitiHex(query.graffiti), - query.fee_recipient, + query.skip_randao_verification, + { + feeRecipient: query.fee_recipient, + builderSelection: query.builder_selection as BuilderSelection, + strictFeeRecipientCheck: query.strict_fee_recipient_check, + }, ], schema: { params: {slot: Schema.UintRequired}, - query: {randao_reveal: Schema.StringRequired, graffiti: Schema.String, fee_recipient: Schema.String}, + query: { + randao_reveal: Schema.StringRequired, + graffiti: Schema.String, + fee_recipient: Schema.String, + skip_randao_verification: Schema.Boolean, + builder_selection: Schema.String, + strict_fee_recipient_check: Schema.Boolean, + }, }, }; @@ -531,9 +604,10 @@ export function getReqSerializers(): ReqSerializers { }, }, - produceBlock: produceBlock, - produceBlockV2: produceBlock, - produceBlindedBlock: produceBlock, + produceBlock: produceBlockV3 as ReqSerializers["produceBlock"], + produceBlockV2: produceBlockV3 as ReqSerializers["produceBlockV2"], + produceBlockV3, + produceBlindedBlock: produceBlockV3 as ReqSerializers["produceBlindedBlock"], produceAttestationData: { writeReq: (index, slot) => ({query: {slot, committee_index: index}}), @@ -641,23 +715,50 @@ export function getReturnTypes(): ReturnTypes { {jsonCase: "eth2"} ); + const produceBlockOrContents = WithExecutionPayloadValue( + WithVersion((fork: ForkName) => + isForkBlobs(fork) ? allForksBlockContentsResSerializer(fork) : ssz[fork].BeaconBlock + ) + ) as TypeJson; + const produceBlindedBlockOrContents = WithExecutionPayloadValue( + WithVersion((fork: ForkName) => + isForkBlobs(fork) + ? allForksBlindedBlockContentsResSerializer(fork) + : ssz.allForksBlinded[isForkExecution(fork) ? fork : ForkName.bellatrix].BeaconBlock + ) + ) as TypeJson; + return { getAttesterDuties: WithDependentRootExecutionOptimistic(ArrayOf(AttesterDuty)), getProposerDuties: WithDependentRootExecutionOptimistic(ArrayOf(ProposerDuty)), getSyncCommitteeDuties: ContainerDataExecutionOptimistic(ArrayOf(SyncDuty)), - produceBlock: WithBlockValue(ContainerData(ssz.phase0.BeaconBlock)), - produceBlockV2: WithBlockValue( - WithVersion((fork: ForkName) => - isForkBlobs(fork) ? AllForksBlockContentsResSerializer(() => fork) : ssz[fork].BeaconBlock - ) - ), - produceBlindedBlock: WithBlockValue( - WithVersion((fork: ForkName) => - isForkBlobs(fork) - ? AllForksBlindedBlockContentsResSerializer(() => fork) - : ssz.allForksBlinded[isForkExecution(fork) ? fork : ForkName.bellatrix].BeaconBlock - ) - ), + + produceBlock: ContainerData(ssz.phase0.BeaconBlock), + produceBlockV2: produceBlockOrContents, + produceBlockV3: { + 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)}; + } else { + return {executionPayloadBlinded: false, ...produceBlockOrContents.fromJson(data)}; + } + }, + }, + produceBlindedBlock: produceBlindedBlockOrContents, + produceAttestationData: ContainerData(ssz.phase0.AttestationData), produceSyncCommitteeContribution: ContainerData(ssz.altair.SyncCommitteeContribution), getAggregatedAttestation: ContainerData(ssz.phase0.Attestation), diff --git a/packages/api/src/builder/routes.ts b/packages/api/src/builder/routes.ts index b3bc3bb38efc..dcff20705c17 100644 --- a/packages/api/src/builder/routes.ts +++ b/packages/api/src/builder/routes.ts @@ -18,7 +18,6 @@ import { import {getReqSerializers as getBeaconReqSerializers} from "../beacon/routes/beacon/block.js"; import {HttpStatusCode} from "../utils/client/httpStatusCode.js"; import {ApiClientResponse} from "../interfaces.js"; -import {SignedBlindedBlockContents} from "../utils/routes.js"; export type Api = { status(): Promise>; @@ -36,7 +35,7 @@ export type Api = { > >; submitBlindedBlock( - signedBlock: allForks.SignedBlindedBeaconBlock | SignedBlindedBlockContents + signedBlock: allForks.SignedBlindedBeaconBlockOrContents ): Promise< ApiClientResponse< {[HttpStatusCode.OK]: {data: allForks.ExecutionPayload; version: ForkName}}, diff --git a/packages/api/src/utils/routes.ts b/packages/api/src/utils/routes.ts index 1763dd23c92a..213a561efd58 100644 --- a/packages/api/src/utils/routes.ts +++ b/packages/api/src/utils/routes.ts @@ -1,50 +1,13 @@ -import {allForks, deneb, ssz} from "@lodestar/types"; +import {allForks, ssz} from "@lodestar/types"; import {ForkBlobs} from "@lodestar/params"; import {TypeJson} from "./types.js"; -export type BlockContents = {block: allForks.BeaconBlock; blobSidecars: deneb.BlobSidecars}; -export type SignedBlockContents = { - signedBlock: allForks.SignedBeaconBlock; - signedBlobSidecars: deneb.SignedBlobSidecars; -}; - -export type BlindedBlockContents = { - blindedBlock: allForks.BlindedBeaconBlock; - blindedBlobSidecars: deneb.BlindedBlobSidecars; -}; -export type SignedBlindedBlockContents = { - signedBlindedBlock: allForks.SignedBlindedBeaconBlock; - signedBlindedBlobSidecars: deneb.SignedBlindedBlobSidecars; -}; - -export function isBlockContents(data: allForks.BeaconBlock | BlockContents): data is BlockContents { - return (data as BlockContents).blobSidecars !== undefined; -} - -export function isSignedBlockContents( - data: allForks.SignedBeaconBlock | SignedBlockContents -): data is SignedBlockContents { - return (data as SignedBlockContents).signedBlobSidecars !== undefined; -} - -export function isBlindedBlockContents( - data: allForks.BlindedBeaconBlock | BlindedBlockContents -): data is BlindedBlockContents { - return (data as BlindedBlockContents).blindedBlobSidecars !== undefined; -} - -export function isSignedBlindedBlockContents( - data: allForks.SignedBlindedBeaconBlock | SignedBlindedBlockContents -): data is SignedBlindedBlockContents { - return (data as SignedBlindedBlockContents).signedBlindedBlobSidecars !== undefined; -} - /* eslint-disable @typescript-eslint/naming-convention */ -export function AllForksSignedBlockContentsReqSerializer( +export function allForksSignedBlockContentsReqSerializer( blockSerializer: (data: allForks.SignedBeaconBlock) => TypeJson -): TypeJson { +): TypeJson { return { toJson: (data) => ({ signed_block: blockSerializer(data.signedBlock).toJson(data.signedBlock), @@ -58,22 +21,22 @@ export function AllForksSignedBlockContentsReqSerializer( }; } -export function AllForksBlockContentsResSerializer(getType: () => ForkBlobs): TypeJson { +export function allForksBlockContentsResSerializer(fork: ForkBlobs): TypeJson { return { toJson: (data) => ({ - block: (ssz.allForks[getType()].BeaconBlock as allForks.AllForksSSZTypes["BeaconBlock"]).toJson(data.block), + block: (ssz.allForks[fork].BeaconBlock as allForks.AllForksSSZTypes["BeaconBlock"]).toJson(data.block), blob_sidecars: ssz.deneb.BlobSidecars.toJson(data.blobSidecars), }), fromJson: (data: {block: unknown; blob_sidecars: unknown}) => ({ - block: ssz.allForks[getType()].BeaconBlock.fromJson(data.block), + block: ssz.allForks[fork].BeaconBlock.fromJson(data.block), blobSidecars: ssz.deneb.BlobSidecars.fromJson(data.blob_sidecars), }), }; } -export function AllForksSignedBlindedBlockContentsReqSerializer( +export function allForksSignedBlindedBlockContentsReqSerializer( blockSerializer: (data: allForks.SignedBlindedBeaconBlock) => TypeJson -): TypeJson { +): TypeJson { return { toJson: (data) => ({ signed_blinded_block: blockSerializer(data.signedBlindedBlock).toJson(data.signedBlindedBlock), @@ -89,16 +52,16 @@ export function AllForksSignedBlindedBlockContentsReqSerializer( }; } -export function AllForksBlindedBlockContentsResSerializer(getType: () => ForkBlobs): TypeJson { +export function allForksBlindedBlockContentsResSerializer(fork: ForkBlobs): TypeJson { return { toJson: (data) => ({ - blinded_block: ( - ssz.allForksBlinded[getType()].BeaconBlock as allForks.AllForksBlindedSSZTypes["BeaconBlock"] - ).toJson(data.blindedBlock), + blinded_block: (ssz.allForksBlinded[fork].BeaconBlock as allForks.AllForksBlindedSSZTypes["BeaconBlock"]).toJson( + data.blindedBlock + ), blinded_blob_sidecars: ssz.deneb.BlindedBlobSidecars.toJson(data.blindedBlobSidecars), }), fromJson: (data: {blinded_block: unknown; blinded_blob_sidecars: unknown}) => ({ - blindedBlock: ssz.allForksBlinded[getType()].BeaconBlock.fromJson(data.blinded_block), + blindedBlock: ssz.allForksBlinded[fork].BeaconBlock.fromJson(data.blinded_block), blindedBlobSidecars: ssz.deneb.BlindedBlobSidecars.fromJson(data.blinded_blob_sidecars), }), }; diff --git a/packages/api/src/utils/schema.ts b/packages/api/src/utils/schema.ts index 03af48195f86..6b08f27bdbab 100644 --- a/packages/api/src/utils/schema.ts +++ b/packages/api/src/utils/schema.ts @@ -34,6 +34,7 @@ export enum Schema { Object, ObjectArray, AnyArray, + Boolean, } /** @@ -68,6 +69,9 @@ function getJsonSchemaItem(schema: Schema): JsonSchema { case Schema.AnyArray: return {type: "array"}; + + case Schema.Boolean: + return {type: "boolean"}; } } diff --git a/packages/api/src/utils/types.ts b/packages/api/src/utils/types.ts index a62fb30b7270..fc7e86a8dbc0 100644 --- a/packages/api/src/utils/types.ts +++ b/packages/api/src/utils/types.ts @@ -29,7 +29,7 @@ export type RouteDef = { export type ReqGeneric = { params?: Record; - query?: Record; + query?: Record; body?: any; headers?: Record; }; @@ -179,18 +179,20 @@ export function WithExecutionOptimistic( } /** - * SSZ factory helper to wrap an existing type with `{blockValue: Wei}` + * SSZ factory helper to wrap an existing type with `{executionPayloadValue: Wei}` */ -export function WithBlockValue(type: TypeJson): TypeJson { +export function WithExecutionPayloadValue( + type: TypeJson +): TypeJson { return { - toJson: ({blockValue, ...data}) => ({ + toJson: ({executionPayloadValue, ...data}) => ({ ...(type.toJson(data as unknown as T) as Record), - block_value: blockValue.toString(), + execution_payload_value: executionPayloadValue.toString(), }), - fromJson: ({block_value, ...data}: T & {block_value: string}) => ({ + fromJson: ({execution_payload_value, ...data}: T & {execution_payload_value: string}) => ({ ...type.fromJson(data), - // For cross client usage where beacon or validator are of separate clients, blockValue could be missing - blockValue: BigInt(block_value ?? "0"), + // For cross client usage where beacon or validator are of separate clients, executionPayloadValue could be missing + executionPayloadValue: BigInt(execution_payload_value ?? "0"), }), }; } diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index 20ad9ef6a099..da245646f8d5 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -45,19 +45,38 @@ export const testData: GenericServerTestCases = { }, }, produceBlock: { - args: [32000, randaoReveal, graffiti, feeRecipient], - res: {data: ssz.phase0.BeaconBlock.defaultValue(), blockValue: ssz.Wei.defaultValue()}, + args: [32000, randaoReveal, graffiti], + res: {data: ssz.phase0.BeaconBlock.defaultValue()}, }, produceBlockV2: { - args: [32000, randaoReveal, graffiti, feeRecipient], - res: {data: ssz.altair.BeaconBlock.defaultValue(), version: ForkName.altair, blockValue: ssz.Wei.defaultValue()}, + args: [32000, randaoReveal, graffiti], + res: { + data: ssz.altair.BeaconBlock.defaultValue(), + version: ForkName.altair, + executionPayloadValue: ssz.Wei.defaultValue(), + }, + }, + produceBlockV3: { + args: [ + 32000, + randaoReveal, + graffiti, + true, + {feeRecipient, builderSelection: undefined, strictFeeRecipientCheck: undefined}, + ], + res: { + data: ssz.altair.BeaconBlock.defaultValue(), + version: ForkName.altair, + executionPayloadValue: ssz.Wei.defaultValue(), + executionPayloadBlinded: false, + }, }, produceBlindedBlock: { - args: [32000, randaoReveal, graffiti, feeRecipient], + args: [32000, randaoReveal, graffiti], res: { data: ssz.bellatrix.BlindedBeaconBlock.defaultValue(), version: ForkName.bellatrix, - blockValue: ssz.Wei.defaultValue(), + executionPayloadValue: ssz.Wei.defaultValue(), }, }, produceAttestationData: { diff --git a/packages/api/test/utils/genericServerTest.ts b/packages/api/test/utils/genericServerTest.ts index 13a1860087e4..d5e091bc25af 100644 --- a/packages/api/test/utils/genericServerTest.ts +++ b/packages/api/test/utils/genericServerTest.ts @@ -58,7 +58,13 @@ export function runGenericServerTest< // Assert server handler called with correct args expect(mockApi[routeId].callCount).to.equal(1, `mockApi[${routeId as string}] must be called once`); - expect(mockApi[routeId].getCall(0).args).to.deep.equal(testCase.args, `mockApi[${routeId as string}] wrong args`); + + // if mock api args are > testcase args, there may be some undefined extra args parsed towards the end + // to obtain a match, ignore the extra args + expect(mockApi[routeId].getCall(0).args.slice(0, testCase.args.length)).to.deep.equal( + testCase.args, + `mockApi[${routeId as string}] wrong args` + ); // Assert returned value is correct expect(res.response).to.deep.equal(testCase.res, "Wrong returned value"); diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 11b96d29fa3b..81eac4ed6e47 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -1,9 +1,9 @@ import {fromHexString, toHexString} from "@chainsafe/ssz"; -import {routes, ServerApi, isSignedBlockContents, isSignedBlindedBlockContents, ResponseFormat} from "@lodestar/api"; -import {computeTimeAtSlot} from "@lodestar/state-transition"; +import {routes, ServerApi, ResponseFormat} from "@lodestar/api"; +import {computeTimeAtSlot, signedBlindedBlockToFull, signedBlindedBlobSidecarsToFull} from "@lodestar/state-transition"; import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; -import {sleep, toHex} from "@lodestar/utils"; -import {allForks, deneb} from "@lodestar/types"; +import {sleep, toHex, LogDataBasic} from "@lodestar/utils"; +import {allForks, deneb, isSignedBlockContents, isSignedBlindedBlockContents} from "@lodestar/types"; import {BlockSource, getBlockInput, ImportBlockOpts, BlockInput} from "../../../../chain/blocks/types.js"; import {promiseAllMaybeAsync} from "../../../../util/promises.js"; import {isOptimisticBlock} from "../../../../util/forkChoice.js"; @@ -15,6 +15,11 @@ import {resolveBlockId, toBeaconHeaderResponse} from "./utils.js"; type PublishBlockOpts = ImportBlockOpts & {broadcastValidation?: routes.beacon.BroadcastValidation}; +type ParsedSignedBlindedBlockOrContents = { + signedBlindedBlock: allForks.SignedBlindedBeaconBlock; + signedBlindedBlobSidecars: deneb.SignedBlindedBlobSidecars | null; +}; + /** * Validator clock may be advanced from beacon's clock. If the validator requests a resource in a * future slot, wait some time instead of rejecting the request because it's in the future @@ -138,17 +143,37 @@ export function getBeaconBlockApi({ signedBlindedBlockOrContents, opts: PublishBlockOpts = {} ) => { - const executionBuilder = chain.executionBuilder; - if (!executionBuilder) throw Error("exeutionBuilder required to publish SignedBlindedBeaconBlock"); - // Mechanism for blobs & blocks on builder is not yet finalized - if (isSignedBlindedBlockContents(signedBlindedBlockOrContents)) { - throw Error("exeutionBuilder not yet implemented for deneb+ forks"); - } else { - const signedBlockOrContents = await executionBuilder.submitBlindedBlock(signedBlindedBlockOrContents); - // the full block is published by relay and it's possible that the block is already known to us by gossip - // see https://github.com/ChainSafe/lodestar/issues/5404 - return publishBlock(signedBlockOrContents, {...opts, ignoreIfKnown: true}); - } + const {signedBlindedBlock, signedBlindedBlobSidecars} = + parseSignedBlindedBlockOrContents(signedBlindedBlockOrContents); + + const slot = signedBlindedBlock.message.slot; + const blockRoot = toHex( + chain.config + .getBlindedForkTypes(signedBlindedBlock.message.slot) + .BeaconBlock.hashTreeRoot(signedBlindedBlock.message) + ); + const logCtx = {blockRoot, slot}; + + // Either the payload/blobs are cached from i) engine locally or ii) they are from the builder + // + // executionPayload can be null or a real payload in locally produced, its only undefined when + // the block came from the builder + const executionPayload = chain.producedBlockRoot.get(blockRoot); + const signedBlockOrContents = + executionPayload !== undefined + ? reconstructLocalBlockOrContents( + chain, + {signedBlindedBlock, signedBlindedBlobSidecars}, + executionPayload, + logCtx + ) + : await reconstructBuilderBlockOrContents(chain, signedBlindedBlockOrContents, logCtx); + + // the full block is published by relay and it's possible that the block is already known to us + // by gossip + // + // see: https://github.com/ChainSafe/lodestar/issues/5404 + return publishBlock(signedBlockOrContents, {...opts, ignoreIfKnown: true}); }; return { @@ -339,3 +364,74 @@ export function getBeaconBlockApi({ }, }; } + +function parseSignedBlindedBlockOrContents( + signedBlindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents +): ParsedSignedBlindedBlockOrContents { + if (isSignedBlindedBlockContents(signedBlindedBlockOrContents)) { + const signedBlindedBlock = signedBlindedBlockOrContents.signedBlindedBlock; + const signedBlindedBlobSidecars = signedBlindedBlockOrContents.signedBlindedBlobSidecars; + return {signedBlindedBlock, signedBlindedBlobSidecars}; + } else { + return {signedBlindedBlock: signedBlindedBlockOrContents, signedBlindedBlobSidecars: null}; + } +} + +function reconstructLocalBlockOrContents( + chain: ApiModules["chain"], + {signedBlindedBlock, signedBlindedBlobSidecars}: ParsedSignedBlindedBlockOrContents, + executionPayload: allForks.ExecutionPayload | null, + logCtx: Record +): allForks.SignedBeaconBlockOrContents { + const signedBlock = signedBlindedBlockToFull(signedBlindedBlock, executionPayload); + if (executionPayload !== null) { + Object.assign(logCtx, {transactions: executionPayload.transactions.length}); + } + + if (signedBlindedBlobSidecars !== null) { + if (executionPayload === null) { + throw Error("Missing locally produced executionPayload for deneb+ publishBlindedBlock"); + } + + const blockHash = toHex(executionPayload.blockHash); + const blobSidecars = chain.producedBlobSidecarsCache.get(blockHash); + if (blobSidecars === undefined) { + throw Error("Missing blobSidecars from the local execution cache"); + } + if (blobSidecars.length !== signedBlindedBlobSidecars.length) { + throw Error( + `Length mismatch signedBlindedBlobSidecars=${signedBlindedBlobSidecars.length} blobSidecars=${blobSidecars.length}` + ); + } + const signedBlobSidecars = signedBlindedBlobSidecarsToFull( + signedBlindedBlobSidecars, + blobSidecars.map((blobSidecar) => blobSidecar.blob) + ); + + Object.assign(logCtx, {blobs: signedBlindedBlobSidecars.length}); + chain.logger.verbose("Block & blobs assembled from locally cached payload", logCtx); + return {signedBlock, signedBlobSidecars} as allForks.SignedBeaconBlockOrContents; + } else { + chain.logger.verbose("Block assembled from locally cached payload", logCtx); + return signedBlock as allForks.SignedBeaconBlockOrContents; + } +} + +async function reconstructBuilderBlockOrContents( + chain: ApiModules["chain"], + signedBlindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents, + logCtx: Record +): Promise { + // Mechanism for blobs & blocks on builder is implemenented separately in a followup deneb-builder PR + if (isSignedBlindedBlockContents(signedBlindedBlockOrContents)) { + throw Error("exeutionBuilder not yet implemented for deneb+ forks"); + } + const executionBuilder = chain.executionBuilder; + if (!executionBuilder) { + throw Error("exeutionBuilder required to publish SignedBlindedBeaconBlock"); + } + + const signedBlockOrContents = await executionBuilder.submitBlindedBlock(signedBlindedBlockOrContents); + chain.logger.verbose("Publishing block assembled from the builder", logCtx); + return signedBlockOrContents; +} diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index c827e121da62..ec3a02dfa26f 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1,5 +1,5 @@ import {fromHexString, toHexString} from "@chainsafe/ssz"; -import {routes, ServerApi, BlockContents} from "@lodestar/api"; +import {routes, ServerApi} from "@lodestar/api"; import { CachedBeaconStateAllForks, computeStartSlotAtEpoch, @@ -8,17 +8,33 @@ import { getBlockRootAtSlot, computeEpochAtSlot, getCurrentSlot, + beaconBlockToBlinded, + blobSidecarsToBlinded, } from "@lodestar/state-transition"; import { GENESIS_SLOT, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT, SYNC_COMMITTEE_SUBNET_SIZE, + isForkBlobs, + isForkExecution, ForkSeq, } from "@lodestar/params"; -import {Root, Slot, ValidatorIndex, ssz, Epoch, ProducedBlockSource, bellatrix, allForks} from "@lodestar/types"; +import { + Root, + Slot, + ValidatorIndex, + ssz, + Epoch, + ProducedBlockSource, + bellatrix, + allForks, + BLSSignature, + isBlindedBeaconBlock, + isBlindedBlockContents, +} from "@lodestar/types"; import {ExecutionStatus} from "@lodestar/fork-choice"; -import {toHex} from "@lodestar/utils"; +import {toHex, racePromisesWithCutoff, RaceEvent} from "@lodestar/utils"; import {AttestationError, AttestationErrorCode, GossipAction, SyncCommitteeError} from "../../../chain/errors/index.js"; import {validateApiAggregateAndProof} from "../../../chain/validation/index.js"; import {ZERO_HASH} from "../../../constants/index.js"; @@ -51,6 +67,18 @@ import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices} from "./utils.js */ const SYNC_TOLERANCE_EPOCHS = 1; +/** + * Cutoff time to wait for execution and builder block production apis to resolve + * Post this time, race execution and builder to pick whatever resolves first + * + * Emprically the builder block resolves in ~1.5+ seconds, and executon should resolve <1 sec. + * 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; + /** * Server implementation for handling validator duties. * See `@lodestar/validator/src/api` for the client implementation). @@ -232,58 +260,85 @@ export function getValidatorApi({ ); } - const produceBlindedBlock: ServerApi["produceBlindedBlock"] = - async function produceBlindedBlock(slot, randaoReveal, graffiti) { - const source = ProducedBlockSource.builder; - let timer; - metrics?.blockProductionRequests.inc({source}); - try { - notWhileSyncing(); - await waitForSlot(slot); // Must never request for a future slot > currentSlot - - // Error early for builder if builder flow not active - if (!chain.executionBuilder) { - throw Error("Execution builder not set"); - } - if (!chain.executionBuilder.status) { - throw Error("Execution builder disabled"); - } + const produceBlindedBlockOrContents = async function produceBlindedBlockOrContents( + slot: Slot, + randaoReveal: BLSSignature, + graffiti: string, + // as of now fee recipient checks can not be performed because builder does not return bid recipient + { + skipHeadChecksAndUpdate, + }: Omit & {skipHeadChecksAndUpdate?: boolean} = {} + ): Promise { + const source = ProducedBlockSource.builder; + metrics?.blockProductionRequests.inc({source}); - // 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.forkChoice.updateHead(); + // Error early for builder if builder flow not active + if (!chain.executionBuilder) { + throw Error("Execution builder not set"); + } + if (!chain.executionBuilder.status) { + throw Error("Execution builder disabled"); + } - timer = metrics?.blockProductionTime.startTimer(); - const {block, blockValue} = await chain.produceBlindedBlock({ - slot, - randaoReveal, - graffiti: toGraffitiBuffer(graffiti || ""), - }); - metrics?.blockProductionSuccess.inc({source}); - metrics?.blockProductionNumAggregated.observe({source}, block.body.attestations.length); - logger.verbose("Produced blinded block", { - slot, - blockValue, - root: toHexString(config.getBlindedForkTypes(slot).BeaconBlock.hashTreeRoot(block)), - }); - return {data: block, version: config.getForkName(block.slot), blockValue}; - } finally { - if (timer) timer({source}); + if (skipHeadChecksAndUpdate !== true) { + 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(); + } + + let timer; + try { + timer = metrics?.blockProductionTime.startTimer(); + const {block, executionPayloadValue} = await chain.produceBlindedBlock({ + slot, + randaoReveal, + graffiti: toGraffitiBuffer(graffiti || ""), + }); + + metrics?.blockProductionSuccess.inc({source}); + metrics?.blockProductionNumAggregated.observe({source}, block.body.attestations.length); + logger.verbose("Produced blinded block", { + slot, + executionPayloadValue, + root: toHexString(config.getBlindedForkTypes(slot).BeaconBlock.hashTreeRoot(block)), + }); + + const version = config.getForkName(block.slot); + if (isForkBlobs(version)) { + if (!isBlindedBlockContents(block)) { + throw Error(`Expected BlockContents response at fork=${version}`); + } + return {data: block, version, executionPayloadValue}; + } else { + if (isBlindedBlockContents(block)) { + throw Error(`Invalid BlockContents response at fork=${version}`); + } + return {data: block, version, executionPayloadValue}; } - }; + } finally { + if (timer) timer({source}); + } + }; - const produceBlockV2: ServerApi["produceBlockV2"] = async function produceBlockV2( - slot, - randaoReveal, - graffiti, - feeRecipient - ) { + const produceFullBlockOrContents = async function produceFullBlockOrContents( + slot: Slot, + randaoReveal: BLSSignature, + graffiti: string, + { + feeRecipient, + strictFeeRecipientCheck, + skipHeadChecksAndUpdate, + }: Omit & {skipHeadChecksAndUpdate?: boolean} = {} + ): Promise { const source = ProducedBlockSource.engine; - let timer; metrics?.blockProductionRequests.inc({source}); - try { + + if (skipHeadChecksAndUpdate !== true) { notWhileSyncing(); await waitForSlot(slot); // Must never request for a future slot > currentSlot @@ -292,53 +347,274 @@ export function getValidatorApi({ // handler, in which case this should just return. chain.forkChoice.updateTime(slot); chain.recomputeForkChoiceHead(); + } + let timer; + try { timer = metrics?.blockProductionTime.startTimer(); - const {block, blockValue} = await chain.produceBlock({ + const {block, executionPayloadValue} = await chain.produceBlock({ slot, randaoReveal, graffiti: toGraffitiBuffer(graffiti || ""), feeRecipient, }); + + const version = config.getForkName(block.slot); + if (strictFeeRecipientCheck && feeRecipient && isForkExecution(version)) { + const blockFeeRecipient = toHexString((block as bellatrix.BeaconBlock).body.executionPayload.feeRecipient); + if (blockFeeRecipient !== feeRecipient) { + throw Error(`Invalid feeRecipient set in engine block expected=${feeRecipient} actual=${blockFeeRecipient}`); + } + } + metrics?.blockProductionSuccess.inc({source}); metrics?.blockProductionNumAggregated.observe({source}, block.body.attestations.length); logger.verbose("Produced execution block", { slot, - blockValue, + executionPayloadValue, root: toHexString(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block)), }); - const version = config.getForkName(block.slot); - if (ForkSeq[version] < ForkSeq.deneb) { - return {data: block, version, blockValue}; - } else { + if (isForkBlobs(version)) { const blockHash = toHex((block as bellatrix.BeaconBlock).body.executionPayload.blockHash); - const {blobSidecars} = chain.producedBlobSidecarsCache.get(blockHash) ?? {}; + const blobSidecars = chain.producedBlobSidecarsCache.get(blockHash); if (blobSidecars === undefined) { throw Error("blobSidecars missing in cache"); } - return {data: {block, blobSidecars} as BlockContents, version, blockValue}; + return {data: {block, blobSidecars} as allForks.BlockContents, version, executionPayloadValue}; + } else { + return {data: block, version, executionPayloadValue}; } } finally { if (timer) timer({source}); } }; + 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("produceBlockV3 assembling block", { + 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 + produceBlindedBlockOrContents(slot, randaoReveal, graffiti, { + feeRecipient, + // skip checking and recomputing head in these individual produce calls + skipHeadChecksAndUpdate: true, + }).catch((e) => { + logger.error("produceBlindedBlockOrContents failed to produce block", {slot}, e); + return null; + }) + : 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 !== routes.validator.BuilderSelection.BuilderOnly + ? // TODO deneb: builderSelection needs to be figured out if to be done beacon side + // || builderSelection !== BuilderSelection.BuilderOnly + produceFullBlockOrContents(slot, randaoReveal, graffiti, { + feeRecipient, + strictFeeRecipientCheck, + // skip checking and recomputing head in these individual produce calls + skipHeadChecksAndUpdate: true, + }).catch((e) => { + logger.error("produceFullBlockOrContents 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.ProduceBlindedBlockOrContentsRes | 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, + }); + } + ); + if (blindedBlock instanceof Error) { + // error here means race cutoff exceeded + logger.error("Failed to produce builder block", {}, blindedBlock); + blindedBlock = null; + } + if (fullBlock instanceof Error) { + 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); + + let selectedSource: ProducedBlockSource | null = null; + + if (fullBlock && blindedBlock) { + switch (builderSelection) { + case routes.validator.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; + } else { + selectedSource = ProducedBlockSource.builder; + } + break; + } + + case routes.validator.BuilderSelection.ExecutionOnly: { + selectedSource = ProducedBlockSource.engine; + break; + } + + // For everything else just select the builder + default: { + selectedSource = ProducedBlockSource.builder; + } + } + logger.verbose(`Selected ${selectedSource} block`, { + builderSelection, + // winston logger doesn't like bigint + enginePayloadValue: `${enginePayloadValue}`, + builderPayloadValue: `${builderPayloadValue}`, + }); + } else if (fullBlock && !blindedBlock) { + selectedSource = ProducedBlockSource.engine; + 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.verbose("Selected builder block: no engine block produced", { + // winston logger doesn't like bigint + builderPayloadValue: `${builderPayloadValue}`, + }); + } + + if (selectedSource === null) { + throw Error("Failed to produce engine or builder block"); + } + + if (selectedSource === ProducedBlockSource.engine) { + return {...fullBlock, executionPayloadBlinded: false} as routes.validator.ProduceBlockOrContentsRes & { + executionPayloadBlinded: false; + }; + } else { + return {...blindedBlock, executionPayloadBlinded: true} as routes.validator.ProduceBlindedBlockOrContentsRes & { + executionPayloadBlinded: true; + }; + } + }; + const produceBlock: ServerApi["produceBlock"] = async function produceBlock( slot, randaoReveal, graffiti ) { - const {data, version, blockValue} = await produceBlockV2(slot, randaoReveal, graffiti, undefined); - if ((data as BlockContents).block !== undefined) { - throw Error(`Invalid block contents for produceBlock at fork=${version}`); + const producedData = await produceFullBlockOrContents(slot, randaoReveal, graffiti); + if (isForkBlobs(producedData.version)) { + throw Error(`Invalid call to produceBlock for deneb+ fork=${producedData.version}`); } else { - return {data: data as allForks.BeaconBlock, version, blockValue}; + // TODO: need to figure out why typescript requires typecasting here + // by typing of produceFullBlockOrContents respose it should have figured this out itself + return producedData as {data: allForks.BeaconBlock}; } }; + const produceBlindedBlock: ServerApi["produceBlindedBlock"] = + async function produceBlindedBlock(slot, randaoReveal, graffiti) { + const producedData = await produceBlockV3(slot, randaoReveal, graffiti); + let blindedProducedData: routes.validator.ProduceBlindedBlockOrContentsRes; + + if (isForkBlobs(producedData.version)) { + if (isBlindedBlockContents(producedData.data as allForks.FullOrBlindedBlockContents)) { + blindedProducedData = producedData as routes.validator.ProduceBlindedBlockOrContentsRes; + } else { + // + const {block, blobSidecars} = producedData.data as allForks.BlockContents; + const blindedBlock = beaconBlockToBlinded(config, block as allForks.AllForksExecution["BeaconBlock"]); + const blindedBlobSidecars = blobSidecarsToBlinded(blobSidecars); + + blindedProducedData = { + ...producedData, + data: {blindedBlock, blindedBlobSidecars}, + } as routes.validator.ProduceBlindedBlockOrContentsRes; + } + } else { + if (isBlindedBeaconBlock(producedData.data)) { + blindedProducedData = producedData as routes.validator.ProduceBlindedBlockOrContentsRes; + } else { + const block = producedData.data; + const blindedBlock = beaconBlockToBlinded(config, block as allForks.AllForksExecution["BeaconBlock"]); + blindedProducedData = { + ...producedData, + data: blindedBlock, + } as routes.validator.ProduceBlindedBlockOrContentsRes; + } + } + return blindedProducedData; + }; + return { - produceBlock: produceBlock, - produceBlockV2: produceBlockV2, + produceBlock, + produceBlockV2: produceFullBlockOrContents, + produceBlockV3, produceBlindedBlock, async produceAttestationData(committeeIndex, slot) { diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 341ad943ee4b..31068d20de27 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -29,7 +29,7 @@ import { import {CheckpointWithHex, ExecutionStatus, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {Logger, isErrorAborted, pruneSetToMax, sleep, toHex} from "@lodestar/utils"; -import {ForkSeq, SLOTS_PER_EPOCH, MAX_BLOBS_PER_BLOCK} from "@lodestar/params"; +import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; import {GENESIS_EPOCH, ZERO_HASH} from "../constants/index.js"; import {IBeaconDb} from "../db/index.js"; @@ -77,13 +77,12 @@ import {BlockInput} from "./blocks/types.js"; import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; /** - * Arbitrary constants, blobs should be consumed immediately in the same slot they are produced. - * A value of 1 would probably be sufficient. However it's sensible to allow some margin if the node overloads. + * Arbitrary constants, blobs and payloads should be consumed immediately in the same slot + * they are produced. A value of 1 would probably be sufficient. However it's sensible to + * allow some margin if the node overloads. */ -const DEFAULT_MAX_CACHED_BLOB_SIDECARS = MAX_BLOBS_PER_BLOCK * 2; -const MAX_RETAINED_SLOTS_CACHED_BLOBS_SIDECAR = 8; -// we have seen two attempts in a single slot so we factor for four const DEFAULT_MAX_CACHED_PRODUCED_ROOTS = 4; +const DEFAULT_MAX_CACHED_BLOB_SIDECARS = 4; export class BeaconChain implements IBeaconChain { readonly genesisTime: UintNum64; @@ -131,13 +130,12 @@ export class BeaconChain implements IBeaconChain { readonly beaconProposerCache: BeaconProposerCache; readonly checkpointBalancesCache: CheckpointBalancesCache; /** Map keyed by executionPayload.blockHash of the block for those blobs */ - readonly producedBlobSidecarsCache = new Map(); - readonly producedBlindedBlobSidecarsCache = new Map< - BlockHash, - {blobSidecars: deneb.BlindedBlobSidecars; slot: Slot} - >(); + readonly producedBlobSidecarsCache = new Map(); - readonly producedBlockRoot = new Set(); + // Cache payload from the local execution so that produceBlindedBlock or produceBlockV3 and + // send and get signed/published blinded versions which beacon can assemble into full before + // actual publish + readonly producedBlockRoot = new Map(); readonly producedBlindedBlockRoot = new Set(); readonly opts: IChainOptions; @@ -461,20 +459,20 @@ export class BeaconChain implements IBeaconChain { return data && {block: data, executionOptimistic: false}; } - produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; blockValue: Wei}> { + produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei}> { return this.produceBlockWrapper(BlockType.Full, blockAttributes); } produceBlindedBlock( blockAttributes: BlockAttributes - ): Promise<{block: allForks.BlindedBeaconBlock; blockValue: Wei}> { + ): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei}> { return this.produceBlockWrapper(BlockType.Blinded, blockAttributes); } async produceBlockWrapper( blockType: T, {randaoReveal, graffiti, slot, feeRecipient}: BlockAttributes - ): Promise<{block: AssembledBlockType; blockValue: Wei}> { + ): Promise<{block: AssembledBlockType; executionPayloadValue: Wei}> { const head = this.forkChoice.getHead(); const state = await this.regen.getBlockSlotState( head.blockRoot, @@ -486,7 +484,7 @@ export class BeaconChain implements IBeaconChain { const proposerIndex = state.epochCtx.getBeaconProposer(slot); const proposerPubKey = state.epochCtx.index2pubkey[proposerIndex].toBytes(); - const {body, blobs, blockValue} = await produceBlockBody.call(this, blockType, state, { + const {body, blobs, executionPayloadValue} = await produceBlockBody.call(this, blockType, state, { randaoReveal, graffiti, slot, @@ -510,9 +508,13 @@ export class BeaconChain implements IBeaconChain { // track the produced block for consensus broadcast validations const blockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block); const blockRootHex = toHex(blockRoot); - const producedRootTracker = blockType === BlockType.Full ? this.producedBlockRoot : this.producedBlindedBlockRoot; - producedRootTracker.add(blockRootHex); - pruneSetToMax(producedRootTracker, this.opts.maxCachedProducedRoots ?? DEFAULT_MAX_CACHED_PRODUCED_ROOTS); + if (blockType === BlockType.Full) { + this.producedBlockRoot.set(blockRootHex, (block as bellatrix.BeaconBlock).body.executionPayload ?? null); + this.metrics?.blockProductionCaches.producedBlockRoot.set(this.producedBlockRoot.size); + } else { + this.producedBlindedBlockRoot.add(blockRootHex); + this.metrics?.blockProductionCaches.producedBlindedBlockRoot.set(this.producedBlindedBlockRoot.size); + } // Cache for latter broadcasting // @@ -529,14 +531,11 @@ export class BeaconChain implements IBeaconChain { proposerIndex, })); - this.producedBlobSidecarsCache.set(blockHash, {blobSidecars, slot}); - pruneSetToMax( - this.producedBlobSidecarsCache, - this.opts.maxCachedBlobSidecars ?? DEFAULT_MAX_CACHED_BLOB_SIDECARS - ); + this.producedBlobSidecarsCache.set(blockHash, blobSidecars); + this.metrics?.blockProductionCaches.producedBlobSidecarsCache.set(this.producedBlobSidecarsCache.size); } - return {block, blockValue}; + return {block, executionPayloadValue}; } /** @@ -551,7 +550,7 @@ export class BeaconChain implements IBeaconChain { */ getBlobSidecars(beaconBlock: deneb.BeaconBlock): deneb.BlobSidecars { const blockHash = toHex(beaconBlock.body.executionPayload.blockHash); - const {blobSidecars} = this.producedBlobSidecarsCache.get(blockHash) ?? {}; + const blobSidecars = this.producedBlobSidecarsCache.get(blockHash); if (!blobSidecars) { throw Error(`No blobSidecars for executionPayload.blockHash ${blockHash}`); } @@ -780,23 +779,19 @@ export class BeaconChain implements IBeaconChain { this.seenAttestationDatas.onSlot(slot); this.reprocessController.onSlot(slot); - // Prune old blobSidecars for block production, those are only useful on their slot - if (this.config.getForkSeq(slot) >= ForkSeq.deneb) { - if (this.producedBlobSidecarsCache.size > 0) { - for (const [key, {slot: blobSlot}] of this.producedBlobSidecarsCache) { - if (slot > blobSlot + MAX_RETAINED_SLOTS_CACHED_BLOBS_SIDECAR) { - this.producedBlobSidecarsCache.delete(key); - } - } - } + // Prune old cached block production artifacts, those are only useful on their slot + pruneSetToMax(this.producedBlockRoot, this.opts.maxCachedProducedRoots ?? DEFAULT_MAX_CACHED_PRODUCED_ROOTS); + this.metrics?.blockProductionCaches.producedBlockRoot.set(this.producedBlockRoot.size); - if (this.producedBlindedBlobSidecarsCache.size > 0) { - for (const [key, {slot: blobSlot}] of this.producedBlindedBlobSidecarsCache) { - if (slot > blobSlot + MAX_RETAINED_SLOTS_CACHED_BLOBS_SIDECAR) { - this.producedBlindedBlobSidecarsCache.delete(key); - } - } - } + pruneSetToMax(this.producedBlindedBlockRoot, this.opts.maxCachedProducedRoots ?? DEFAULT_MAX_CACHED_PRODUCED_ROOTS); + this.metrics?.blockProductionCaches.producedBlindedBlockRoot.set(this.producedBlockRoot.size); + + if (this.config.getForkSeq(slot) >= ForkSeq.deneb) { + pruneSetToMax( + this.producedBlobSidecarsCache, + this.opts.maxCachedBlobSidecars ?? DEFAULT_MAX_CACHED_BLOB_SIDECARS + ); + this.metrics?.blockProductionCaches.producedBlobSidecarsCache.set(this.producedBlobSidecarsCache.size); } const metrics = this.metrics; diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 211ac7e3777a..b7f33555545a 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -93,9 +93,8 @@ export interface IBeaconChain { readonly beaconProposerCache: BeaconProposerCache; readonly checkpointBalancesCache: CheckpointBalancesCache; - readonly producedBlobSidecarsCache: Map; - readonly producedBlindedBlobSidecarsCache: Map; - readonly producedBlockRoot: Set; + readonly producedBlobSidecarsCache: Map; + readonly producedBlockRoot: Map; readonly producedBlindedBlockRoot: Set; readonly opts: IChainOptions; @@ -138,8 +137,10 @@ export interface IBeaconChain { getBlobSidecars(beaconBlock: deneb.BeaconBlock): deneb.BlobSidecars; - produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; blockValue: Wei}>; - produceBlindedBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BlindedBeaconBlock; blockValue: Wei}>; + produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei}>; + produceBlindedBlock( + blockAttributes: BlockAttributes + ): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei}>; /** Process a block until complete */ processBlock(block: BlockInput, opts?: ImportBlockOpts): Promise; diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 1ab87b392150..5224aae65035 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -92,12 +92,12 @@ export async function produceBlockBody( proposerIndex: ValidatorIndex; proposerPubKey: BLSPubkey; } -): Promise<{body: AssembledBodyType; blobs: BlobsResult; blockValue: Wei}> { +): Promise<{body: AssembledBodyType; blobs: BlobsResult; executionPayloadValue: Wei}> { // 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 let blobsResult: BlobsResult; - let blockValue: Wei; + let executionPayloadValue: Wei; const fork = currentState.config.getForkName(blockSlot); const logMeta: Record = { @@ -194,8 +194,8 @@ export async function produceBlockBody( proposerPubKey ); (blockBody as allForks.BlindedBeaconBlockBody).executionPayloadHeader = builderRes.header; - blockValue = builderRes.blockValue; - this.logger.verbose("Fetched execution payload header from builder", {slot: blockSlot, blockValue}); + executionPayloadValue = builderRes.executionPayloadValue; + this.logger.verbose("Fetched execution payload header from builder", {slot: blockSlot, executionPayloadValue}); if (ForkSeq[fork] >= ForkSeq.deneb) { const {blobKzgCommitments} = builderRes; if (blobKzgCommitments === undefined) { @@ -232,7 +232,7 @@ export async function produceBlockBody( (blockBody as allForks.ExecutionBlockBody).executionPayload = ssz.allForksExecution[fork].ExecutionPayload.defaultValue(); blobsResult = {type: BlobsResultType.preDeneb}; - blockValue = BigInt(0); + executionPayloadValue = BigInt(0); } else { const {prepType, payloadId} = prepareRes; Object.assign(logMeta, {executionPayloadPrepType: prepType}); @@ -249,14 +249,14 @@ export async function produceBlockBody( const engineRes = await this.executionEngine.getPayload(fork, payloadId); const {executionPayload, blobsBundle} = engineRes; (blockBody as allForks.ExecutionBlockBody).executionPayload = executionPayload; - blockValue = engineRes.blockValue; + executionPayloadValue = engineRes.executionPayloadValue; Object.assign(logMeta, {transactions: executionPayload.transactions.length}); 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, - blockValue, + executionPayloadValue, prepType, payloadId, fetchedTime, @@ -311,7 +311,7 @@ export async function produceBlockBody( (blockBody as allForks.ExecutionBlockBody).executionPayload = ssz.allForksExecution[fork].ExecutionPayload.defaultValue(); blobsResult = {type: BlobsResultType.preDeneb}; - blockValue = BigInt(0); + executionPayloadValue = BigInt(0); } else { // since merge transition is complete, we need a valid payload even if with an // empty (transactions) one. defaultValue isn't gonna cut it! @@ -321,7 +321,7 @@ export async function produceBlockBody( } } else { blobsResult = {type: BlobsResultType.preDeneb}; - blockValue = BigInt(0); + executionPayloadValue = BigInt(0); } if (ForkSeq[fork] >= ForkSeq.capella) { @@ -339,10 +339,10 @@ export async function produceBlockBody( } } - Object.assign(logMeta, {blockValue}); + Object.assign(logMeta, {executionPayloadValue}); this.logger.verbose("Produced beacon block body", logMeta); - return {body: blockBody as AssembledBodyType, blobs: blobsResult, blockValue}; + return {body: blockBody as AssembledBodyType, blobs: blobsResult, executionPayloadValue}; } /** @@ -442,7 +442,7 @@ async function prepareExecutionPayloadHeader( proposerPubKey: BLSPubkey ): Promise<{ header: allForks.ExecutionPayloadHeader; - blockValue: Wei; + executionPayloadValue: Wei; blobKzgCommitments?: deneb.BlobKzgCommitments; }> { if (!chain.executionBuilder) { diff --git a/packages/beacon-node/src/execution/builder/http.ts b/packages/beacon-node/src/execution/builder/http.ts index 1bbc7b090314..9a423e0f832d 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -96,14 +96,14 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { proposerPubKey: BLSPubkey ): Promise<{ header: allForks.ExecutionPayloadHeader; - blockValue: Wei; + executionPayloadValue: Wei; blobKzgCommitments?: deneb.BlobKzgCommitments; }> { const res = await this.api.getHeader(slot, parentHash, proposerPubKey); ApiError.assert(res, "execution.builder.getheader"); - const {header, value: blockValue} = res.response.data.message; + const {header, value: executionPayloadValue} = res.response.data.message; const {blobKzgCommitments} = res.response.data.message as {blobKzgCommitments?: deneb.BlobKzgCommitments}; - return {header, blockValue, blobKzgCommitments}; + return {header, executionPayloadValue, blobKzgCommitments}; } async submitBlindedBlock(signedBlock: allForks.SignedBlindedBeaconBlock): Promise { diff --git a/packages/beacon-node/src/execution/builder/interface.ts b/packages/beacon-node/src/execution/builder/interface.ts index 6e008e30f828..2bc7a19765a0 100644 --- a/packages/beacon-node/src/execution/builder/interface.ts +++ b/packages/beacon-node/src/execution/builder/interface.ts @@ -22,7 +22,7 @@ export interface IExecutionBuilder { proposerPubKey: BLSPubkey ): Promise<{ header: allForks.ExecutionPayloadHeader; - blockValue: Wei; + executionPayloadValue: Wei; blobKzgCommitments?: deneb.BlobKzgCommitments; }>; submitBlindedBlock(signedBlock: allForks.SignedBlindedBeaconBlock): Promise; diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index cf3865286ea5..70df97ba1e4a 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -363,7 +363,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { async getPayload( fork: ForkName, payloadId: PayloadId - ): Promise<{executionPayload: allForks.ExecutionPayload; blockValue: Wei; blobsBundle?: BlobsBundle}> { + ): Promise<{executionPayload: allForks.ExecutionPayload; executionPayloadValue: Wei; blobsBundle?: BlobsBundle}> { const method = ForkSeq[fork] >= ForkSeq.deneb ? "engine_getPayloadV3" diff --git a/packages/beacon-node/src/execution/engine/interface.ts b/packages/beacon-node/src/execution/engine/interface.ts index 18a037095805..9a7ee3963379 100644 --- a/packages/beacon-node/src/execution/engine/interface.ts +++ b/packages/beacon-node/src/execution/engine/interface.ts @@ -136,7 +136,7 @@ export interface IExecutionEngine { getPayload( fork: ForkName, payloadId: PayloadId - ): Promise<{executionPayload: allForks.ExecutionPayload; blockValue: Wei; blobsBundle?: BlobsBundle}>; + ): Promise<{executionPayload: allForks.ExecutionPayload; executionPayloadValue: Wei; blobsBundle?: BlobsBundle}>; getPayloadBodiesByHash(blockHash: DATA[]): Promise<(ExecutionPayloadBody | null)[]>; diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index e686c8ece513..4f24480e0b96 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -102,12 +102,13 @@ export type EngineApiRpcReturnTypes = { engine_getPayloadBodiesByRangeV1: (ExecutionPayloadBodyRpc | null)[]; }; -type ExecutionPayloadRpcWithBlockValue = { +type ExecutionPayloadRpcWithValue = { executionPayload: ExecutionPayloadRpc; + // even though CL tracks this as executionPayloadValue, EL returns this as blockValue blockValue: QUANTITY; blobsBundle?: BlobsBundleRpc; }; -type ExecutionPayloadResponse = ExecutionPayloadRpc | ExecutionPayloadRpcWithBlockValue; +type ExecutionPayloadResponse = ExecutionPayloadRpc | ExecutionPayloadRpcWithValue; export type ExecutionPayloadBodyRpc = {transactions: DATA[]; withdrawals: WithdrawalV1[] | null}; @@ -199,25 +200,25 @@ export function serializeVersionedHashes(vHashes: VersionedHashes): VersionedHas return vHashes.map(bytesToData); } -export function hasBlockValue(response: ExecutionPayloadResponse): response is ExecutionPayloadRpcWithBlockValue { - return (response as ExecutionPayloadRpcWithBlockValue).blockValue !== undefined; +export function hasPayloadValue(response: ExecutionPayloadResponse): response is ExecutionPayloadRpcWithValue { + return (response as ExecutionPayloadRpcWithValue).blockValue !== undefined; } export function parseExecutionPayload( fork: ForkName, response: ExecutionPayloadResponse -): {executionPayload: allForks.ExecutionPayload; blockValue: Wei; blobsBundle?: BlobsBundle} { +): {executionPayload: allForks.ExecutionPayload; executionPayloadValue: Wei; blobsBundle?: BlobsBundle} { let data: ExecutionPayloadRpc; - let blockValue: Wei; + let executionPayloadValue: Wei; let blobsBundle: BlobsBundle | undefined; - if (hasBlockValue(response)) { - blockValue = quantityToBigint(response.blockValue); + if (hasPayloadValue(response)) { + executionPayloadValue = quantityToBigint(response.blockValue); data = response.executionPayload; blobsBundle = response.blobsBundle ? parseBlobsBundle(response.blobsBundle) : undefined; } else { data = response; // Just set it to zero as default - blockValue = BigInt(0); + executionPayloadValue = BigInt(0); blobsBundle = undefined; } @@ -268,7 +269,7 @@ export function parseExecutionPayload( (executionPayload as deneb.ExecutionPayload).excessBlobGas = quantityToBigint(excessBlobGas); } - return {executionPayload, blockValue, blobsBundle}; + return {executionPayload, executionPayloadValue, blobsBundle}; } export function serializePayloadAttributes(data: PayloadAttributes): PayloadAttributesRpc { diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index 25f842e635af..9ea233b18fa2 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -138,6 +138,21 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { labelNames: ["source"], }), + blockProductionCaches: { + producedBlockRoot: register.gauge({ + name: "beacon_blockroot_produced_cache_total", + help: "Count of cached produded block roots", + }), + producedBlindedBlockRoot: register.gauge({ + name: "beacon_blinded_blockroot_produced_cache_total", + help: "Count of cached produded blinded block roots", + }), + producedBlobSidecarsCache: register.gauge({ + name: "beacon_blobsidecars_produced_cache_total", + help: "Count of cached produced blob sidecars", + }), + }, + blockPayload: { payloadAdvancePrepTime: register.histogram({ name: "beacon_block_payload_prepare_time", diff --git a/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts b/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts index bb325a17b25e..6e8056ea7458 100644 --- a/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts +++ b/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts @@ -5,6 +5,7 @@ import {config as defaultConfig} from "@lodestar/config/default"; import {ChainForkConfig} from "@lodestar/config"; import {BeaconChain} from "../../src/chain/index.js"; import {ExecutionEngineHttp} from "../../src/execution/engine/http.js"; +import {ExecutionBuilderHttp} from "../../src/execution/builder/http.js"; import {Eth1ForBlockProduction} from "../../src/eth1/index.js"; import {OpPool} from "../../src/chain/opPools/opPool.js"; import {AggregatedAttestationPool} from "../../src/chain/opPools/aggregatedAttestationPool.js"; @@ -18,6 +19,7 @@ export type MockedBeaconChain = MockedObject & { getHeadState: Mock<[]>; forkChoice: MockedObject; executionEngine: MockedObject; + executionBuilder: MockedObject; eth1: MockedObject; opPool: MockedObject; aggregatedAttestationPool: MockedObject; @@ -33,6 +35,7 @@ export type MockedBeaconChain = MockedObject & { }; vi.mock("@lodestar/fork-choice"); vi.mock("../../src/execution/engine/http.js"); +vi.mock("../../src/execution/builder/http.js"); vi.mock("../../src/eth1/index.js"); vi.mock("../../src/chain/opPools/opPool.js"); vi.mock("../../src/chain/opPools/aggregatedAttestationPool.js"); @@ -63,6 +66,9 @@ vi.mock("../../src/chain/index.js", async (requireActual) => { executionEngine: new ExecutionEngineHttp(), // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error + executionBuilder: new ExecutionBuilderHttp(), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error eth1: new Eth1ForBlockProduction(), opPool: new OpPool(), aggregatedAttestationPool: new AggregatedAttestationPool(), @@ -70,6 +76,7 @@ vi.mock("../../src/chain/index.js", async (requireActual) => { // @ts-expect-error beaconProposerCache: new BeaconProposerCache(), produceBlock: vi.fn(), + produceBlindedBlock: vi.fn(), getCanonicalBlockAtSlot: vi.fn(), recomputeForkChoiceHead: vi.fn(), getHeadStateAtCurrentEpoch: vi.fn(), diff --git a/packages/beacon-node/test/sim/merge-interop.test.ts b/packages/beacon-node/test/sim/merge-interop.test.ts index c9b7f3989d62..a9fa10ab2208 100644 --- a/packages/beacon-node/test/sim/merge-interop.test.ts +++ b/packages/beacon-node/test/sim/merge-interop.test.ts @@ -315,14 +315,13 @@ describe("executionEngine / ExecutionEngineHttp", function () { strictFeeRecipientCheck: true, feeRecipient: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", builder: { - enabled: false, gasLimit: 30000000, + builderSelection: "executiononly", }, }, "0xa4855c83d868f772a579133d9f23818008417b743e8447e235d8eb78b1d8f8a9f63f98c551beb7de254400f89592314d": { feeRecipient: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", builder: { - enabled: true, gasLimit: 35000000, }, }, @@ -332,7 +331,6 @@ describe("executionEngine / ExecutionEngineHttp", function () { strictFeeRecipientCheck: true, feeRecipient: "0xcccccccccccccccccccccccccccccccccccccccc", builder: { - enabled: false, gasLimit: 30000000, }, }, diff --git a/packages/beacon-node/test/sim/mergemock.test.ts b/packages/beacon-node/test/sim/mergemock.test.ts index c1efa6d6837e..0761005714bd 100644 --- a/packages/beacon-node/test/sim/mergemock.test.ts +++ b/packages/beacon-node/test/sim/mergemock.test.ts @@ -6,7 +6,7 @@ import {TimestampFormatCode} from "@lodestar/logger"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {ChainConfig} from "@lodestar/config"; import {Epoch, allForks, bellatrix} from "@lodestar/types"; -import {ValidatorProposerConfig, BuilderSelection} from "@lodestar/validator"; +import {ValidatorProposerConfig} from "@lodestar/validator"; import {routes} from "@lodestar/api"; import {ClockEvent} from "../../src/util/clock.js"; @@ -22,8 +22,7 @@ import {logFilesDir} from "./params.js"; import {shell} from "./shell.js"; // NOTE: How to run -// EL_BINARY_DIR=g11tech/mergemock:latest EL_SCRIPT_DIR=mergemock LODESTAR_PRESET=mainnet \ -// ETH_PORT=8661 ENGINE_PORT=8551 yarn mocha test/sim/mergemock.test.ts +// EL_BINARY_DIR=g11tech/mergemock:latest EL_SCRIPT_DIR=mergemock LODESTAR_PRESET=mainnet ETH_PORT=8661 ENGINE_PORT=8551 yarn mocha test/sim/mergemock.test.ts // ``` /* eslint-disable no-console, @typescript-eslint/naming-convention */ @@ -64,25 +63,30 @@ describe("executionEngine / ExecutionEngineHttp", function () { } }); - it("Post-merge, run for a few blocks", async function () { - console.log("\n\nPost-merge, run for a few blocks\n\n"); - const {elClient, tearDownCallBack} = await runEL( - {...elSetupConfig, mode: ELStartMode.PostMerge}, - {...elRunOptions, ttd: BigInt(0)}, - controller.signal - ); - afterEachCallbacks.push(() => tearDownCallBack()); + for (const useProduceBlockV3 of [false, true]) { + it(`Test builder with useProduceBlockV3=${useProduceBlockV3}`, async function () { + console.log("\n\nPost-merge, run for a few blocks\n\n"); + const {elClient, tearDownCallBack} = await runEL( + {...elSetupConfig, mode: ELStartMode.PostMerge}, + {...elRunOptions, ttd: BigInt(0)}, + controller.signal + ); + afterEachCallbacks.push(() => tearDownCallBack()); - await runNodeWithEL.bind(this)({ - elClient, - bellatrixEpoch: 0, - testName: "post-merge", + await runNodeWithEL.bind(this)({ + elClient, + bellatrixEpoch: 0, + testName: "post-merge", + useProduceBlockV3, + }); }); - }); + } + + type RunOpts = {elClient: ELClient; bellatrixEpoch: Epoch; testName: string; useProduceBlockV3: boolean}; async function runNodeWithEL( this: Context, - {elClient, bellatrixEpoch, testName}: {elClient: ELClient; bellatrixEpoch: Epoch; testName: string} + {elClient, bellatrixEpoch, testName, useProduceBlockV3}: RunOpts ): Promise { const {genesisBlockHash, ttd, engineRpcUrl, ethRpcUrl} = elClient; const validatorClientCount = 1; @@ -184,9 +188,8 @@ describe("executionEngine / ExecutionEngineHttp", function () { strictFeeRecipientCheck: true, feeRecipient: feeRecipientEngine, builder: { - enabled: true, gasLimit: 30000000, - selection: BuilderSelection.BuilderAlways, + selection: routes.validator.BuilderSelection.BuilderAlways, }, }, } as ValidatorProposerConfig; @@ -201,6 +204,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { useRestApi: true, testLoggerOpts, valProposerConfig, + useProduceBlockV3, }); afterEachCallbacks.push(async function () { diff --git a/packages/beacon-node/test/sim/withdrawal-interop.test.ts b/packages/beacon-node/test/sim/withdrawal-interop.test.ts index 8067565fc84c..b828382ad247 100644 --- a/packages/beacon-node/test/sim/withdrawal-interop.test.ts +++ b/packages/beacon-node/test/sim/withdrawal-interop.test.ts @@ -27,7 +27,7 @@ import {logFilesDir} from "./params.js"; import {shell} from "./shell.js"; // NOTE: How to run -// EL_BINARY_DIR=g11tech/geth:withdrawals EL_SCRIPT_DIR=gethdocker yarn mocha test/sim/withdrawal-interop.test.ts +// EL_BINARY_DIR=g11tech/geth:withdrawalsfeb8 EL_SCRIPT_DIR=gethdocker yarn mocha test/sim/withdrawal-interop.test.ts // ``` /* eslint-disable no-console, @typescript-eslint/naming-convention */ @@ -160,8 +160,8 @@ describe("executionEngine / ExecutionEngineHttp", function () { if (!payloadId) throw Error("InvalidPayloadId"); // 2. Get the payload - const payloadAndBlockValue = await executionEngine.getPayload(ForkName.capella, payloadId); - const payload = payloadAndBlockValue.executionPayload; + const payloadWithValue = await executionEngine.getPayload(ForkName.capella, payloadId); + const payload = payloadWithValue.executionPayload; const stateRoot = toHexString(payload.stateRoot); const expectedStateRoot = "0x6160c5b91ea5ded26da07f6655762deddefdbed6ddab2edc60484cfb38ef16be"; diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV2.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV2.test.ts index f349ed36314b..febb027303b7 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV2.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV2.test.ts @@ -71,7 +71,7 @@ describe("api/validator - produceBlockV2", function () { }; const fullBlock = ssz.bellatrix.BeaconBlock.defaultValue(); - const blockValue = ssz.Wei.defaultValue(); + const executionPayloadValue = ssz.Wei.defaultValue(); const currentSlot = 100000; vi.spyOn(server.chainStub.clock, "currentSlot", "get").mockReturnValue(currentSlot); @@ -81,18 +81,18 @@ describe("api/validator - produceBlockV2", function () { const slot = 100000; const randaoReveal = fullBlock.body.randaoReveal; const graffiti = "a".repeat(32); - const expectedFeeRecipient = "0xcccccccccccccccccccccccccccccccccccccccc"; + const feeRecipient = "0xcccccccccccccccccccccccccccccccccccccccc"; const api = getValidatorApi(modules); - server.chainStub.produceBlock.mockResolvedValue({block: fullBlock, blockValue}); + server.chainStub.produceBlock.mockResolvedValue({block: fullBlock, executionPayloadValue}); // check if expectedFeeRecipient is passed to produceBlock - await api.produceBlockV2(slot, randaoReveal, graffiti, expectedFeeRecipient); + await api.produceBlockV2(slot, randaoReveal, graffiti, {feeRecipient}); expect(server.chainStub.produceBlock).toBeCalledWith({ randaoReveal, graffiti: toGraffitiBuffer(graffiti), slot, - feeRecipient: expectedFeeRecipient, + feeRecipient, }); // check that no feeRecipient is passed to produceBlock so that produceBlockBody will @@ -108,11 +108,11 @@ describe("api/validator - produceBlockV2", function () { it("correctly use passed feeRecipient in notifyForkchoiceUpdate", async () => { const fullBlock = ssz.bellatrix.BeaconBlock.defaultValue(); - const blockValue = ssz.Wei.defaultValue(); + const executionPayloadValue = ssz.Wei.defaultValue(); const slot = 100000; const randaoReveal = fullBlock.body.randaoReveal; const graffiti = "a".repeat(32); - const expectedFeeRecipient = "0xccccccccccccccccccccccccccccccccccccccaa"; + const feeRecipient = "0xccccccccccccccccccccccccccccccccccccccaa"; const headSlot = 0; forkChoiceStub.getHead.mockReturnValue(generateProtoBlock({slot: headSlot})); @@ -127,7 +127,7 @@ describe("api/validator - produceBlockV2", function () { executionEngineStub.notifyForkchoiceUpdate.mockResolvedValue("0x"); executionEngineStub.getPayload.mockResolvedValue({ executionPayload: ssz.bellatrix.ExecutionPayload.defaultValue(), - blockValue, + executionPayloadValue, }); // use fee recipient passed in produceBlockBody call for payload gen in engine notifyForkchoiceUpdate @@ -135,7 +135,7 @@ describe("api/validator - produceBlockV2", function () { randaoReveal, graffiti: toGraffitiBuffer(graffiti), slot, - feeRecipient: expectedFeeRecipient, + feeRecipient, parentSlot: slot - 1, parentBlockRoot: fromHexString(ZERO_HASH_HEX), proposerIndex: 0, @@ -150,7 +150,7 @@ describe("api/validator - produceBlockV2", function () { { timestamp: computeTimeAtSlot(chainStub.config, state.slot, state.genesisTime), prevRandao: Uint8Array.from(Buffer.alloc(32, 0)), - suggestedFeeRecipient: expectedFeeRecipient, + suggestedFeeRecipient: feeRecipient, } ); 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 new file mode 100644 index 000000000000..0835777dd7ec --- /dev/null +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts @@ -0,0 +1,133 @@ +import {describe, it, expect, beforeEach, afterEach, MockedObject, vi} from "vitest"; +import {ssz} from "@lodestar/types"; +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {routes} from "@lodestar/api"; +import {createBeaconConfig, createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {SyncState} from "../../../../../src/sync/interface.js"; +import {ApiModules} from "../../../../../src/api/impl/types.js"; +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"; + +/* eslint-disable @typescript-eslint/naming-convention */ +describe("api/validator - produceBlockV3", function () { + const logger = testLogger(); + + let modules: ApiModules; + let server: ApiImplTestModules; + + let chainStub: ApiImplTestModules["chainStub"]; + let executionBuilderStub: MockedObject; + let syncStub: ApiImplTestModules["syncStub"]; + + const chainConfig = createChainForkConfig({ + ...defaultChainConfig, + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 1, + }); + const genesisValidatorsRoot = Buffer.alloc(32, 0xaa); + const config = createBeaconConfig(chainConfig, genesisValidatorsRoot); + + beforeEach(() => { + server = setupApiImplTestServer(); + chainStub = server.chainStub; + executionBuilderStub = server.chainStub.executionBuilder; + syncStub = server.syncStub; + + executionBuilderStub.status = true; + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + const testCases: [routes.validator.BuilderSelection, number | null, number | null, string][] = [ + [routes.validator.BuilderSelection.MaxProfit, 1, 0, "builder"], + [routes.validator.BuilderSelection.MaxProfit, 1, 2, "engine"], + [routes.validator.BuilderSelection.MaxProfit, null, 0, "engine"], + [routes.validator.BuilderSelection.MaxProfit, 0, null, "builder"], + + [routes.validator.BuilderSelection.BuilderAlways, 1, 2, "builder"], + [routes.validator.BuilderSelection.BuilderAlways, 1, 0, "builder"], + [routes.validator.BuilderSelection.BuilderAlways, null, 0, "engine"], + [routes.validator.BuilderSelection.BuilderAlways, 0, null, "builder"], + + [routes.validator.BuilderSelection.BuilderOnly, 0, 2, "builder"], + [routes.validator.BuilderSelection.ExecutionOnly, 2, 0, "execution"], + ]; + + testCases.forEach(([builderSelection, builderPayloadValue, enginePayloadValue, finalSelection]) => { + it(`produceBlockV3 - ${finalSelection} produces block`, async () => { + syncStub = server.syncStub; + modules = { + chain: server.chainStub, + config, + db: server.dbStub, + logger, + network: server.networkStub, + sync: syncStub, + metrics: null, + }; + + const fullBlock = ssz.bellatrix.BeaconBlock.defaultValue(); + const blindedBlock = ssz.bellatrix.BlindedBeaconBlock.defaultValue(); + + const slot = 1 * SLOTS_PER_EPOCH; + const randaoReveal = fullBlock.body.randaoReveal; + const graffiti = "a".repeat(32); + const feeRecipient = "0xccccccccccccccccccccccccccccccccccccccaa"; + const currentSlot = 1 * SLOTS_PER_EPOCH; + + vi.spyOn(server.chainStub.clock, "currentSlot", "get").mockReturnValue(currentSlot); + vi.spyOn(syncStub, "state", "get").mockReturnValue(SyncState.Synced); + + const api = getValidatorApi(modules); + + if (enginePayloadValue !== null) { + chainStub.produceBlock.mockResolvedValue({ + block: fullBlock, + executionPayloadValue: BigInt(enginePayloadValue), + }); + } else { + chainStub.produceBlock.mockRejectedValue(Error("not produced")); + } + + if (builderPayloadValue !== null) { + chainStub.produceBlindedBlock.mockResolvedValue({ + block: blindedBlock, + executionPayloadValue: BigInt(builderPayloadValue), + }); + } else { + chainStub.produceBlindedBlock.mockRejectedValue(Error("not produced")); + } + + const _skipRandaoVerification = false; + const produceBlockOpts = { + strictFeeRecipientCheck: false, + builderSelection, + feeRecipient, + }; + + const block = await api.produceBlockV3(slot, randaoReveal, graffiti, _skipRandaoVerification, produceBlockOpts); + + const expectedBlock = finalSelection === "builder" ? blindedBlock : fullBlock; + const expectedExecution = finalSelection === "builder" ? true : false; + + expect(block.data).toEqual(expectedBlock); + expect(block.executionPayloadBlinded).toEqual(expectedExecution); + + // check call counts + if (builderSelection === routes.validator.BuilderSelection.ExecutionOnly) { + expect(chainStub.produceBlindedBlock).toBeCalledTimes(0); + } else { + expect(chainStub.produceBlindedBlock).toBeCalledTimes(1); + } + + if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) { + expect(chainStub.produceBlock).toBeCalledTimes(0); + } else { + expect(chainStub.produceBlock).toBeCalledTimes(1); + } + }); + }); +}); diff --git a/packages/beacon-node/test/unit/executionEngine/http.test.ts b/packages/beacon-node/test/unit/executionEngine/http.test.ts index 8955048a4cd8..250b433214ce 100644 --- a/packages/beacon-node/test/unit/executionEngine/http.test.ts +++ b/packages/beacon-node/test/unit/executionEngine/http.test.ts @@ -81,8 +81,8 @@ describe("ExecutionEngine / http", () => { }; returnValue = response; - const payloadAndBlockValue = await executionEngine.getPayload(ForkName.bellatrix, "0x0"); - const payload = payloadAndBlockValue.executionPayload; + const payloadWithValue = await executionEngine.getPayload(ForkName.bellatrix, "0x0"); + const payload = payloadWithValue.executionPayload; expect(serializeExecutionPayload(ForkName.bellatrix, payload)).toEqual(response.result); expect(reqJsonRpcPayload).toEqual(request); diff --git a/packages/beacon-node/test/utils/node/validator.ts b/packages/beacon-node/test/utils/node/validator.ts index 0a567ca17320..dfa627383306 100644 --- a/packages/beacon-node/test/utils/node/validator.ts +++ b/packages/beacon-node/test/utils/node/validator.ts @@ -19,6 +19,7 @@ export async function getAndInitDevValidators({ externalSignerUrl, doppelgangerProtection = false, valProposerConfig, + useProduceBlockV3, }: { node: BeaconNode; logPrefix: string; @@ -30,6 +31,7 @@ export async function getAndInitDevValidators({ externalSignerUrl?: string; doppelgangerProtection?: boolean; valProposerConfig?: ValidatorProposerConfig; + useProduceBlockV3?: boolean; }): Promise<{validators: Validator[]; secretKeys: SecretKey[]}> { const validators: Promise[] = []; const secretKeys: SecretKey[] = []; @@ -74,6 +76,7 @@ export async function getAndInitDevValidators({ signers, doppelgangerProtection, valProposerConfig, + useProduceBlockV3, }) ); } diff --git a/packages/cli/src/cmds/validator/handler.ts b/packages/cli/src/cmds/validator/handler.ts index 69ead593d1e6..703398d4f026 100644 --- a/packages/cli/src/cmds/validator/handler.ts +++ b/packages/cli/src/cmds/validator/handler.ts @@ -1,13 +1,8 @@ import path from "node:path"; import {setMaxListeners} from "node:events"; import {LevelDbController} from "@lodestar/db"; -import { - ProcessShutdownCallback, - SlashingProtection, - Validator, - ValidatorProposerConfig, - BuilderSelection, -} from "@lodestar/validator"; +import {ProcessShutdownCallback, SlashingProtection, Validator, ValidatorProposerConfig} from "@lodestar/validator"; +import {routes} from "@lodestar/api"; import {getMetrics, MetricsRegister} from "@lodestar/validator"; import { RegistryMetricCreator, @@ -167,6 +162,7 @@ export async function validatorHandler(args: IValidatorCliArgs & GlobalArgs): Pr disableAttestationGrouping: args.disableAttestationGrouping, valProposerConfig, distributed: args.distributed, + useProduceBlockV3: args.useProduceBlockV3, }, metrics ); @@ -219,7 +215,6 @@ function getProposerConfigFromArgs( strictFeeRecipientCheck: args.strictFeeRecipientCheck, feeRecipient: args.suggestedFeeRecipient ? parseFeeRecipient(args.suggestedFeeRecipient) : undefined, builder: { - enabled: args.builder, gasLimit: args.defaultGasLimit, selection: parseBuilderSelection(args["builder.selection"]), }, @@ -248,7 +243,7 @@ function getProposerConfigFromArgs( return valProposerConfig; } -function parseBuilderSelection(builderSelection?: string): BuilderSelection | undefined { +function parseBuilderSelection(builderSelection?: string): routes.validator.BuilderSelection | undefined { if (builderSelection) { switch (builderSelection) { case "maxprofit": @@ -257,9 +252,11 @@ function parseBuilderSelection(builderSelection?: string): BuilderSelection | un break; case "builderonly": break; + case "executiononly": + break; default: throw Error("Invalid input for builder selection, check help."); } } - return builderSelection as BuilderSelection; + return builderSelection as routes.validator.BuilderSelection; } diff --git a/packages/cli/src/cmds/validator/options.ts b/packages/cli/src/cmds/validator/options.ts index 609a06164c2c..8daa1feda4bf 100644 --- a/packages/cli/src/cmds/validator/options.ts +++ b/packages/cli/src/cmds/validator/options.ts @@ -46,6 +46,8 @@ export type IValidatorCliArgs = AccountValidatorArgs & builder?: boolean; "builder.selection"?: string; + useProduceBlockV3?: boolean; + importKeystores?: string[]; importKeystoresPassword?: string; @@ -233,6 +235,7 @@ export const validatorOptions: CliCommandOptions = { type: "boolean", description: "Enable execution payload production via a builder for better rewards", group: "builder", + deprecated: "enabling or disabling builder flow is now solely managed by `builder.selection` flag", }, "builder.selection": { @@ -242,6 +245,12 @@ export const validatorOptions: CliCommandOptions = { group: "builder", }, + useProduceBlockV3: { + type: "boolean", + description: "Enable/disable usage of produceBlockV3 that might not be supported by all beacon clients yet", + defaultDescription: `${defaultOptions.useProduceBlockV3}`, + }, + 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/cli/src/util/proposerConfig.ts b/packages/cli/src/util/proposerConfig.ts index 29fcb25a48ea..2f3c71236255 100644 --- a/packages/cli/src/util/proposerConfig.ts +++ b/packages/cli/src/util/proposerConfig.ts @@ -2,7 +2,9 @@ import fs from "node:fs"; import path from "node:path"; -import {ValidatorProposerConfig, BuilderSelection} from "@lodestar/validator"; +import {ValidatorProposerConfig} from "@lodestar/validator"; +import {routes} from "@lodestar/api"; + import {parseFeeRecipient} from "./feeRecipient.js"; import {readFile} from "./file.js"; @@ -16,9 +18,8 @@ type ProposerConfigFileSection = { builder?: { // boolean are parse as string by the default schema readFile employs // for js-yaml - enabled?: string; gas_limit?: number; - selection?: BuilderSelection; + selection?: routes.validator.BuilderSelection; }; }; @@ -56,7 +57,7 @@ function parseProposerConfigSection( overrideConfig?: ProposerConfig ): ProposerConfig { const {graffiti, strict_fee_recipient_check, fee_recipient, builder} = proposerFileSection; - const {enabled, gas_limit, selection: builderSelection} = builder || {}; + const {gas_limit, selection: builderSelection} = builder || {}; if (graffiti !== undefined && typeof graffiti !== "string") { throw Error("graffiti is not 'string"); @@ -65,14 +66,11 @@ function parseProposerConfigSection( strict_fee_recipient_check !== undefined && !(strict_fee_recipient_check === "true" || strict_fee_recipient_check === "false") ) { - throw Error("enabled is not set to boolean"); + throw Error("strict_fee_recipient_check is not set to boolean"); } if (fee_recipient !== undefined && typeof fee_recipient !== "string") { throw Error("fee_recipient is not 'string"); } - if (enabled !== undefined && !(enabled === "true" || enabled === "false")) { - throw Error("enabled is not set to boolean"); - } if (gas_limit !== undefined) { if (typeof gas_limit !== "string") { throw Error("(typeof gas_limit !== 'string') 2 "); @@ -89,7 +87,6 @@ function parseProposerConfigSection( (strict_fee_recipient_check ? stringtoBool(strict_fee_recipient_check) : undefined), feeRecipient: overrideConfig?.feeRecipient ?? (fee_recipient ? parseFeeRecipient(fee_recipient) : undefined), builder: { - enabled: overrideConfig?.builder?.enabled ?? (enabled ? stringtoBool(enabled) : undefined), gasLimit: overrideConfig?.builder?.gasLimit ?? (gas_limit !== undefined ? Number(gas_limit) : undefined), selection: overrideConfig?.builder?.selection ?? builderSelection, }, diff --git a/packages/cli/test/sim/mixed_client.test.ts b/packages/cli/test/sim/mixed_client.test.ts index ccb498865bae..35588f8d7c6f 100644 --- a/packages/cli/test/sim/mixed_client.test.ts +++ b/packages/cli/test/sim/mixed_client.test.ts @@ -60,7 +60,21 @@ const env = await SimulationEnvironment.initWithDefaults( keysCount: 32, remote: true, beacon: BeaconClient.Lighthouse, - validator: ValidatorClient.Lodestar, + // for cross client make sure lodestar doesn't use v3 for now untill lighthouse supports + validator: { + type: ValidatorClient.Lodestar, + options: { + clientOptions: { + useProduceBlockV3: false, + // this should cause usage of produceBlockV2 + // + // but if blinded production is enabled in lighthouse beacon then this should cause + // usage of produce blinded block which should return execution block in blinded format + // but only enable that after testing lighthouse beacon + "builder.selection": "executiononly", + }, + }, + }, }, ] ); diff --git a/packages/cli/test/sim/multi_fork.test.ts b/packages/cli/test/sim/multi_fork.test.ts index 117ad42e53eb..2cc07445ce95 100644 --- a/packages/cli/test/sim/multi_fork.test.ts +++ b/packages/cli/test/sim/multi_fork.test.ts @@ -3,7 +3,7 @@ import path from "node:path"; import {sleep, toHex, toHexString} from "@lodestar/utils"; import {ApiError} from "@lodestar/api"; import {CLIQUE_SEALING_PERIOD, SIM_TESTS_SECONDS_PER_SLOT} from "../utils/simulation/constants.js"; -import {AssertionMatch, BeaconClient, ExecutionClient} from "../utils/simulation/interfaces.js"; +import {AssertionMatch, BeaconClient, ExecutionClient, ValidatorClient} from "../utils/simulation/interfaces.js"; import {SimulationEnvironment} from "../utils/simulation/SimulationEnvironment.js"; import {getEstimatedTimeInSecForRun, getEstimatedTTD, logFilesDir} from "../utils/simulation/utils/index.js"; import { @@ -56,9 +56,60 @@ const env = await SimulationEnvironment.initWithDefaults( }, }, [ - {id: "node-1", beacon: BeaconClient.Lodestar, execution: ExecutionClient.Geth, keysCount: 32, mining: true}, - {id: "node-2", beacon: BeaconClient.Lodestar, execution: ExecutionClient.Nethermind, keysCount: 32, remote: true}, - {id: "node-3", beacon: BeaconClient.Lodestar, execution: ExecutionClient.Nethermind, keysCount: 32}, + // put 1 lodestar node on produceBlockV3, and 2nd on produceBlindedBlock and 3rd on produceBlockV2 + // specifying the useProduceBlockV3 options despite whatever default is set + { + id: "node-1", + beacon: BeaconClient.Lodestar, + validator: { + type: ValidatorClient.Lodestar, + options: { + clientOptions: { + useProduceBlockV3: true, + // default builder selection will cause a race try in beacon even if builder is not set + // but not to worry, execution block will be selected as fallback anyway + }, + }, + }, + execution: ExecutionClient.Geth, + keysCount: 32, + mining: true, + }, + { + id: "node-2", + beacon: BeaconClient.Lodestar, + validator: { + type: ValidatorClient.Lodestar, + options: { + clientOptions: { + useProduceBlockV3: false, + // default builder selection of max profit will make it use produceBlindedBlock + // but not to worry, execution block will be selected as fallback anyway + // but returned in blinded format for validator to use publish blinded block + // which assembles block beacon side from local cache before publishing + }, + }, + }, + execution: ExecutionClient.Nethermind, + keysCount: 32, + remote: true, + }, + { + id: "node-3", + beacon: BeaconClient.Lodestar, + validator: { + type: ValidatorClient.Lodestar, + options: { + clientOptions: { + useProduceBlockV3: false, + // this builder selection will make it use produceBlockV2 + "builder.selection": "executiononly", + }, + }, + }, + execution: ExecutionClient.Nethermind, + keysCount: 32, + }, {id: "node-4", beacon: BeaconClient.Lighthouse, execution: ExecutionClient.Geth, keysCount: 32}, ] ); diff --git a/packages/cli/test/unit/validator/parseProposerConfig.test.ts b/packages/cli/test/unit/validator/parseProposerConfig.test.ts index ba4a7610f521..da459cf84c6e 100644 --- a/packages/cli/test/unit/validator/parseProposerConfig.test.ts +++ b/packages/cli/test/unit/validator/parseProposerConfig.test.ts @@ -2,7 +2,7 @@ import path from "node:path"; import {fileURLToPath} from "node:url"; import {expect} from "chai"; -import {BuilderSelection} from "@lodestar/validator"; +import {routes} from "@lodestar/api"; import {parseProposerConfig} from "../../../src/util/index.js"; @@ -15,7 +15,6 @@ const testValue = { strictFeeRecipientCheck: true, feeRecipient: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", builder: { - enabled: true, gasLimit: 30000000, selection: undefined, }, @@ -25,9 +24,8 @@ const testValue = { strictFeeRecipientCheck: undefined, feeRecipient: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", builder: { - enabled: true, gasLimit: 35000000, - selection: BuilderSelection.MaxProfit, + selection: routes.validator.BuilderSelection.MaxProfit, }, }, }, @@ -36,9 +34,8 @@ const testValue = { strictFeeRecipientCheck: true, feeRecipient: "0xcccccccccccccccccccccccccccccccccccccccc", builder: { - enabled: true, gasLimit: 30000000, - selection: BuilderSelection.BuilderAlways, + selection: routes.validator.BuilderSelection.BuilderAlways, }, }, }; diff --git a/packages/cli/test/unit/validator/proposerConfigs/validData.yaml b/packages/cli/test/unit/validator/proposerConfigs/validData.yaml index 2d954e85d7b3..6b7e7074b118 100644 --- a/packages/cli/test/unit/validator/proposerConfigs/validData.yaml +++ b/packages/cli/test/unit/validator/proposerConfigs/validData.yaml @@ -4,12 +4,10 @@ proposer_config: strict_fee_recipient_check: true fee_recipient: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' builder: - enabled: true gas_limit: "30000000" '0xa4855c83d868f772a579133d9f23818008417b743e8447e235d8eb78b1d8f8a9f63f98c551beb7de254400f89592314d': fee_recipient: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' builder: - enabled: "true" gas_limit: "35000000" selection: "maxprofit" default_config: @@ -17,6 +15,5 @@ default_config: strict_fee_recipient_check: "true" fee_recipient: '0xcccccccccccccccccccccccccccccccccccccccc' builder: - enabled: true gas_limit: "30000000" selection: "builderalways" diff --git a/packages/cli/test/utils/simulation/validator_clients/lodestar.ts b/packages/cli/test/utils/simulation/validator_clients/lodestar.ts index 43b852bd4c6d..a85347d780c5 100644 --- a/packages/cli/test/utils/simulation/validator_clients/lodestar.ts +++ b/packages/cli/test/utils/simulation/validator_clients/lodestar.ts @@ -5,6 +5,7 @@ import got from "got"; import {getClient as keyManagerGetClient} from "@lodestar/api/keymanager"; import {chainConfigToJson} from "@lodestar/config"; import {LogLevel} from "@lodestar/utils"; +import {defaultOptions} from "@lodestar/validator"; import {IValidatorCliArgs} from "../../../../src/cmds/validator/options.js"; import {GlobalArgs} from "../../../../src/options/globalOptions.js"; import {LODESTAR_BINARY_PATH} from "../constants.js"; @@ -12,8 +13,9 @@ import {RunnerType, ValidatorClient, ValidatorNodeGenerator} from "../interfaces import {getNodePorts} from "../utils/ports.js"; export const generateLodestarValidatorNode: ValidatorNodeGenerator = (opts, runner) => { - const {paths, id, keys, forkConfig, genesisTime, nodeIndex, beaconUrls} = opts; + const {paths, id, keys, forkConfig, genesisTime, nodeIndex, beaconUrls, clientOptions} = opts; const {rootDir, keystoresDir, keystoresSecretFilePath, logFilePath} = paths; + const {useProduceBlockV3, "builder.selection": builderSelection} = clientOptions ?? {}; const ports = getNodePorts(nodeIndex); const rcConfigPath = path.join(rootDir, "rc_config.json"); const paramsPath = path.join(rootDir, "params.json"); @@ -37,6 +39,8 @@ export const generateLodestarValidatorNode: ValidatorNodeGenerator; +export type ForkPreLightClient = ForkName.phase0; +export type ForkLightClient = Exclude; export function isForkLightClient(fork: ForkName): fork is ForkLightClient { return fork !== ForkName.phase0; } -export type ForkExecution = Exclude; +export type ForkPreExecution = ForkPreLightClient | ForkName.altair; +export type ForkExecution = Exclude; export function isForkExecution(fork: ForkName): fork is ForkExecution { return isForkLightClient(fork) && fork !== ForkName.altair; } -export type ForkWithdrawals = Exclude; +export type ForkPreWithdrawals = ForkPreExecution | ForkName.bellatrix; +export type ForkWithdrawals = Exclude; export function isForkWithdrawals(fork: ForkName): fork is ForkWithdrawals { return isForkExecution(fork) && fork !== ForkName.bellatrix; } -export type ForkBlobs = Exclude; +export type ForkPreBlobs = ForkPreWithdrawals | ForkName.capella; +export type ForkBlobs = Exclude; export function isForkBlobs(fork: ForkName): fork is ForkBlobs { return isForkWithdrawals(fork) && fork !== ForkName.capella; } diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index cdf9d7e8d144..3d784356ae0f 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -6,8 +6,7 @@ import {presetStatus} from "./presetStatus.js"; import {userSelectedPreset, userOverrides} from "./setPreset.js"; export type {BeaconPreset} from "./types.js"; -export type {ForkLightClient, ForkExecution, ForkBlobs} from "./forkName.js"; -export {ForkName, ForkSeq, isForkExecution, isForkWithdrawals, isForkBlobs, isForkLightClient} from "./forkName.js"; +export * from "./forkName.js"; export {presetToJson} from "./json.js"; export {PresetName}; diff --git a/packages/state-transition/src/block/processExecutionPayload.ts b/packages/state-transition/src/block/processExecutionPayload.ts index 27091774cc20..b589436012a5 100644 --- a/packages/state-transition/src/block/processExecutionPayload.ts +++ b/packages/state-transition/src/block/processExecutionPayload.ts @@ -1,9 +1,14 @@ import {toHexString, byteArrayEquals} from "@chainsafe/ssz"; -import {ssz, allForks, capella, deneb} from "@lodestar/types"; +import {allForks, deneb} from "@lodestar/types"; import {ForkSeq, MAX_BLOBS_PER_BLOCK} from "@lodestar/params"; import {CachedBeaconStateBellatrix, CachedBeaconStateCapella} from "../types.js"; import {getRandaoMix} from "../util/index.js"; -import {isExecutionPayload, isMergeTransitionComplete, getFullOrBlindedPayloadFromBody} from "../util/execution.js"; +import { + isExecutionPayload, + isMergeTransitionComplete, + getFullOrBlindedPayloadFromBody, + executionPayloadToPayloadHeader, +} from "../util/execution.js"; import {BlockExternalData, ExecutionPayloadStatus} from "./externalData.js"; export function processExecutionPayload( @@ -77,45 +82,3 @@ export function processExecutionPayload( .getExecutionForkTypes(state.slot) .ExecutionPayloadHeader.toViewDU(payloadHeader) as typeof state.latestExecutionPayloadHeader; } - -export function executionPayloadToPayloadHeader( - fork: ForkSeq, - payload: allForks.ExecutionPayload -): allForks.ExecutionPayloadHeader { - const transactionsRoot = ssz.bellatrix.Transactions.hashTreeRoot(payload.transactions); - - const bellatrixPayloadFields: allForks.ExecutionPayloadHeader = { - parentHash: payload.parentHash, - feeRecipient: payload.feeRecipient, - stateRoot: payload.stateRoot, - receiptsRoot: payload.receiptsRoot, - logsBloom: payload.logsBloom, - prevRandao: payload.prevRandao, - blockNumber: payload.blockNumber, - gasLimit: payload.gasLimit, - gasUsed: payload.gasUsed, - timestamp: payload.timestamp, - extraData: payload.extraData, - baseFeePerGas: payload.baseFeePerGas, - blockHash: payload.blockHash, - transactionsRoot, - }; - - if (fork >= ForkSeq.capella) { - (bellatrixPayloadFields as capella.ExecutionPayloadHeader).withdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot( - (payload as capella.ExecutionPayload).withdrawals - ); - } - - if (fork >= ForkSeq.deneb) { - // https://github.com/ethereum/consensus-specs/blob/dev/specs/eip4844/beacon-chain.md#process_execution_payload - (bellatrixPayloadFields as deneb.ExecutionPayloadHeader).blobGasUsed = ( - payload as deneb.ExecutionPayloadHeader | deneb.ExecutionPayload - ).blobGasUsed; - (bellatrixPayloadFields as deneb.ExecutionPayloadHeader).excessBlobGas = ( - payload as deneb.ExecutionPayloadHeader | deneb.ExecutionPayload - ).excessBlobGas; - } - - return bellatrixPayloadFields; -} diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index f9db0bb84887..433aa45cf7e6 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -59,4 +59,3 @@ export {ExecutionPayloadStatus, DataAvailableStatus, type BlockExternalData} fro export {becomesNewEth1Data} from "./block/processEth1Data.js"; // Withdrawals for new blocks export {getExpectedWithdrawals} from "./block/processWithdrawals.js"; -export {executionPayloadToPayloadHeader} from "./block/processExecutionPayload.js"; diff --git a/packages/state-transition/src/util/blindedBlock.ts b/packages/state-transition/src/util/blindedBlock.ts index 3b0ecc469597..02f0397a33e7 100644 --- a/packages/state-transition/src/util/blindedBlock.ts +++ b/packages/state-transition/src/util/blindedBlock.ts @@ -1,5 +1,8 @@ import {ChainForkConfig} from "@lodestar/config"; -import {allForks, phase0, Root, isBlindedBeaconBlock, isBlindedBlobSidecar} from "@lodestar/types"; +import {ForkSeq} from "@lodestar/params"; +import {allForks, phase0, Root, isBlindedBeaconBlock, isBlindedBlobSidecar, deneb, ssz} from "@lodestar/types"; + +import {executionPayloadToPayloadHeader} from "./execution.js"; export function blindedOrFullBlockHashTreeRoot( config: ChainForkConfig, @@ -41,3 +44,58 @@ export function blindedOrFullBlockToHeader( bodyRoot, }; } + +export function beaconBlockToBlinded( + config: ChainForkConfig, + block: allForks.AllForksExecution["BeaconBlock"] +): allForks.BlindedBeaconBlock { + const fork = config.getForkName(block.slot); + const executionPayloadHeader = executionPayloadToPayloadHeader(ForkSeq[fork], block.body.executionPayload); + const blindedBlock = {...block, body: {...block.body, executionPayloadHeader}} as allForks.BlindedBeaconBlock; + return blindedBlock; +} + +export function blobSidecarsToBlinded(blobSidecars: deneb.BlobSidecars): deneb.BlindedBlobSidecars { + return blobSidecars.map((blobSidecar) => { + const blobRoot = ssz.deneb.Blob.hashTreeRoot(blobSidecar.blob); + return {...blobSidecar, blobRoot} as deneb.BlindedBlobSidecar; + }); +} + +export function signedBlindedBlockToFull( + signedBlindedBlock: allForks.SignedBlindedBeaconBlock, + executionPayload: allForks.ExecutionPayload | null +): allForks.SignedBeaconBlock { + const signedBlock = { + ...signedBlindedBlock, + message: { + ...signedBlindedBlock.message, + body: { + ...signedBlindedBlock.message.body, + // state transition doesn't handle null value for executionPayload in pre-bellatrix blocks + executionPayload: executionPayload ?? undefined, + }, + }, + } as allForks.SignedBeaconBlock; + + // state transition can't seem to handle executionPayloadHeader presense in merge block + // so just delete the extra field we don't require + delete (signedBlock.message.body as {executionPayloadHeader?: allForks.ExecutionPayloadHeader}) + .executionPayloadHeader; + return signedBlock; +} + +export function signedBlindedBlobSidecarsToFull( + signedBlindedBlobSidecars: deneb.SignedBlindedBlobSidecars, + blobs: deneb.Blobs +): deneb.SignedBlobSidecars { + const signedBlobSidecars = signedBlindedBlobSidecars.map((signedBlindedBlobSidecar, index) => { + const signedBlobSidecar = { + ...signedBlindedBlobSidecar, + message: {...signedBlindedBlobSidecar.message, blob: blobs[index]}, + }; + delete (signedBlobSidecar.message as {blobRoot?: deneb.BlindedBlob}).blobRoot; + return signedBlobSidecar; + }); + return signedBlobSidecars; +} diff --git a/packages/state-transition/src/util/execution.ts b/packages/state-transition/src/util/execution.ts index d31d8c0d9e15..7ac4da4aeecb 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -1,4 +1,6 @@ -import {allForks, bellatrix, capella, isBlindedBeaconBlockBody, ssz} from "@lodestar/types"; +import {allForks, bellatrix, capella, deneb, isBlindedBeaconBlockBody, ssz} from "@lodestar/types"; +import {ForkSeq} from "@lodestar/params"; + import { BeaconStateBellatrix, BeaconStateCapella, @@ -128,3 +130,45 @@ export function isCapellaPayloadHeader( ): payload is capella.ExecutionPayloadHeader { return (payload as capella.ExecutionPayloadHeader).withdrawalsRoot !== undefined; } + +export function executionPayloadToPayloadHeader( + fork: ForkSeq, + payload: allForks.ExecutionPayload +): allForks.ExecutionPayloadHeader { + const transactionsRoot = ssz.bellatrix.Transactions.hashTreeRoot(payload.transactions); + + const bellatrixPayloadFields: allForks.ExecutionPayloadHeader = { + parentHash: payload.parentHash, + feeRecipient: payload.feeRecipient, + stateRoot: payload.stateRoot, + receiptsRoot: payload.receiptsRoot, + logsBloom: payload.logsBloom, + prevRandao: payload.prevRandao, + blockNumber: payload.blockNumber, + gasLimit: payload.gasLimit, + gasUsed: payload.gasUsed, + timestamp: payload.timestamp, + extraData: payload.extraData, + baseFeePerGas: payload.baseFeePerGas, + blockHash: payload.blockHash, + transactionsRoot, + }; + + if (fork >= ForkSeq.capella) { + (bellatrixPayloadFields as capella.ExecutionPayloadHeader).withdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot( + (payload as capella.ExecutionPayload).withdrawals + ); + } + + if (fork >= ForkSeq.deneb) { + // https://github.com/ethereum/consensus-specs/blob/dev/specs/eip4844/beacon-chain.md#process_execution_payload + (bellatrixPayloadFields as deneb.ExecutionPayloadHeader).blobGasUsed = ( + payload as deneb.ExecutionPayloadHeader | deneb.ExecutionPayload + ).blobGasUsed; + (bellatrixPayloadFields as deneb.ExecutionPayloadHeader).excessBlobGas = ( + payload as deneb.ExecutionPayloadHeader | deneb.ExecutionPayload + ).excessBlobGas; + } + + return bellatrixPayloadFields; +} diff --git a/packages/types/src/allForks/types.ts b/packages/types/src/allForks/types.ts index 779485cb73da..a525820aac02 100644 --- a/packages/types/src/allForks/types.ts +++ b/packages/types/src/allForks/types.ts @@ -72,6 +72,27 @@ export type FullOrBlindedBlobSidecar = deneb.BlobSidecar | deneb.BlindedBlobSide export type FullOrBlindedSignedBlobSidecar = deneb.SignedBlobSidecar | deneb.SignedBlindedBlobSidecar; export type FullOrBlindedBlobSidecars = deneb.BlobSidecars | deneb.BlindedBlobSidecars; +export type BlockContents = {block: BeaconBlock; blobSidecars: deneb.BlobSidecars}; +export type SignedBlockContents = { + signedBlock: SignedBeaconBlock; + signedBlobSidecars: deneb.SignedBlobSidecars; +}; + +export type BlindedBlockContents = { + blindedBlock: BlindedBeaconBlock; + blindedBlobSidecars: deneb.BlindedBlobSidecars; +}; +export type SignedBlindedBlockContents = { + signedBlindedBlock: SignedBlindedBeaconBlock; + signedBlindedBlobSidecars: deneb.SignedBlindedBlobSidecars; +}; + +export type FullOrBlindedBlockContents = BlockContents | BlindedBlockContents; +export type FullOrBlindedBeaconBlockOrContents = FullOrBlindedBeaconBlock | FullOrBlindedBlockContents; +export type BeaconBlockOrContents = BeaconBlock | BlockContents; +export type BlindedBeaconBlockOrContents = BlindedBeaconBlock | BlindedBlockContents; +export type SignedBeaconBlockOrContents = SignedBeaconBlock | SignedBlockContents; +export type SignedBlindedBeaconBlockOrContents = SignedBlindedBeaconBlock | SignedBlindedBlockContents; export type BuilderBid = bellatrix.BuilderBid | capella.BuilderBid | deneb.BuilderBid; export type SignedBuilderBid = bellatrix.SignedBuilderBid | capella.SignedBuilderBid | deneb.SignedBuilderBid; diff --git a/packages/types/src/utils/typeguards.ts b/packages/types/src/utils/typeguards.ts index 0303caa10dbf..a3e4393c51cb 100644 --- a/packages/types/src/utils/typeguards.ts +++ b/packages/types/src/utils/typeguards.ts @@ -1,4 +1,5 @@ import { + FullOrBlindedBeaconBlockOrContents, FullOrBlindedBeaconBlock, FullOrBlindedSignedBeaconBlock, FullOrBlindedBeaconBlockBody, @@ -8,8 +9,14 @@ import { FullOrBlindedSignedBlobSidecar, BlindedBeaconBlockBody, BlindedBeaconBlock, + BlockContents, + SignedBlindedBlockContents, + SignedBlindedBeaconBlock, + BlindedBlockContents, + SignedBlockContents, + SignedBeaconBlock, + SignedBlindedBeaconBlockOrContents, } from "../allForks/types.js"; -import {ts as bellatrix} from "../bellatrix/index.js"; import {ts as deneb} from "../deneb/index.js"; export function isBlindedExecution(payload: FullOrBlindedExecutionPayload): payload is ExecutionPayloadHeader { @@ -18,8 +25,9 @@ export function isBlindedExecution(payload: FullOrBlindedExecutionPayload): payl return (payload as ExecutionPayloadHeader).transactionsRoot !== undefined; } -export function isBlindedBeaconBlock(block: FullOrBlindedBeaconBlock): block is BlindedBeaconBlock { - return isBlindedBeaconBlockBody(block.body); +export function isBlindedBeaconBlock(block: FullOrBlindedBeaconBlockOrContents): block is BlindedBeaconBlock { + const body = (block as FullOrBlindedBeaconBlock).body; + return body !== undefined && isBlindedBeaconBlockBody(body); } export function isBlindedBeaconBlockBody(body: FullOrBlindedBeaconBlockBody): body is BlindedBeaconBlockBody { @@ -28,8 +36,8 @@ export function isBlindedBeaconBlockBody(body: FullOrBlindedBeaconBlockBody): bo export function isBlindedSignedBeaconBlock( signedBlock: FullOrBlindedSignedBeaconBlock -): signedBlock is bellatrix.SignedBlindedBeaconBlock { - return (signedBlock as bellatrix.SignedBlindedBeaconBlock).message.body.executionPayloadHeader !== undefined; +): signedBlock is SignedBlindedBeaconBlock { + return (signedBlock as SignedBlindedBeaconBlock).message.body.executionPayloadHeader !== undefined; } export function isBlindedBlobSidecar(blob: FullOrBlindedBlobSidecar): blob is deneb.BlindedBlobSidecar { @@ -41,3 +49,21 @@ export function isBlindedSignedBlobSidecar( ): blob is deneb.SignedBlindedBlobSidecar { return (blob as deneb.SignedBlindedBlobSidecar).message.blobRoot !== undefined; } + +export function isBlockContents(data: FullOrBlindedBeaconBlockOrContents): data is BlockContents { + return (data as BlockContents).blobSidecars !== undefined; +} + +export function isSignedBlockContents(data: SignedBeaconBlock | SignedBlockContents): data is SignedBlockContents { + return (data as SignedBlockContents).signedBlobSidecars !== undefined; +} + +export function isBlindedBlockContents(data: FullOrBlindedBeaconBlockOrContents): data is BlindedBlockContents { + return (data as BlindedBlockContents).blindedBlobSidecars !== undefined; +} + +export function isSignedBlindedBlockContents( + data: SignedBlindedBeaconBlockOrContents +): data is SignedBlindedBlockContents { + return (data as SignedBlindedBlockContents).signedBlindedBlobSidecars !== undefined; +} diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index 622ec823cb5f..925a357f4b98 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -17,5 +17,5 @@ export const LogLevels = Object.values(LogLevel); export type LogHandler = (message: string, context?: LogData, error?: Error) => void; -type LogDataBasic = string | number | bigint | boolean | null | undefined; +export type LogDataBasic = string | number | bigint | boolean | null | undefined; export type LogData = LogDataBasic | Record | LogDataBasic[] | Record[]; diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts index 35e05303e615..6582b660d011 100644 --- a/packages/validator/src/index.ts +++ b/packages/validator/src/index.ts @@ -1,5 +1,5 @@ export {Validator, type ValidatorOptions} from "./validator.js"; -export {ValidatorStore, SignerType, defaultOptions, BuilderSelection} from "./services/validatorStore.js"; +export {ValidatorStore, SignerType, defaultOptions} from "./services/validatorStore.js"; export type { Signer, SignerLocal, diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index 81c749297f48..bbe96ac772a8 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -4,56 +4,59 @@ 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 { - Api, - ApiError, isBlockContents, isBlindedBlockContents, - SignedBlindedBlockContents, - SignedBlockContents, - BlockContents, - BlindedBlockContents, -} from "@lodestar/api"; +} from "@lodestar/types"; +import {ChainForkConfig} from "@lodestar/config"; +import {ForkPreBlobs, ForkBlobs, ForkSeq} from "@lodestar/params"; +import {extendError, prettyBytes} from "@lodestar/utils"; +import {Api, ApiError, routes} from "@lodestar/api"; import {IClock, LoggerVc} from "../util/index.js"; import {PubkeyHex} from "../types.js"; import {Metrics} from "../metrics.js"; import {formatBigDecimal} from "../util/format.js"; -import {ValidatorStore, BuilderSelection} from "./validatorStore.js"; +import {ValidatorStore} from "./validatorStore.js"; import {BlockDutiesService, GENESIS_SLOT} from "./blockDuties.js"; const ETH_TO_WEI = BigInt("1000000000000000000"); // display upto 5 decimal places const MAX_DECIMAL_FACTOR = BigInt("100000"); -/** - * Cutoff time to wait for execution and builder block production apis to resolve - * Post this time, race execution and builder to pick whatever resolves first - * - * Emprically the builder block resolves in ~1.5+ seconds, and executon should resolve <1 sec. - * 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; - -type ProduceBlockOpts = { - expectedFeeRecipient: string; - strictFeeRecipientCheck: boolean; - isBuilderEnabled: boolean; - builderSelection: BuilderSelection; -}; +// The following combination of blocks and blobs can be produced +// i) a full block pre deneb +// ii) a full block and full blobs post deneb +// iii) a blinded block pre deneb as a result of beacon/execution race +// iv) a blinded block + blinded blobs as a result of beacon/execution race +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; + }; +type DebugLogCtx = {debugLogCtx: Record}; /** * Service that sets up and handles validator block proposal duties. */ @@ -66,7 +69,8 @@ export class BlockProposingService { private readonly api: Api, private readonly clock: IClock, private readonly validatorStore: ValidatorStore, - private readonly metrics: Metrics | null + private readonly metrics: Metrics | null, + private readonly opts: {useProduceBlockV3: boolean} ) { this.dutiesService = new BlockDutiesService( config, @@ -115,23 +119,22 @@ export class BlockProposingService { const debugLogCtx = {...logCtx, validator: pubkeyHex}; const strictFeeRecipientCheck = this.validatorStore.strictFeeRecipientCheck(pubkeyHex); - const isBuilderEnabled = this.validatorStore.isBuilderEnabled(pubkeyHex); const builderSelection = this.validatorStore.getBuilderSelection(pubkeyHex); - const expectedFeeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex); + const feeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex); this.logger.debug("Producing block", { ...debugLogCtx, - isBuilderEnabled, builderSelection, - expectedFeeRecipient, + feeRecipient, strictFeeRecipientCheck, + useProduceBlockV3: this.opts.useProduceBlockV3, }); this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot)); - const blockContents = await this.produceBlockWrapper(slot, randaoReveal, graffiti, { - expectedFeeRecipient, + const produceBlockFn = this.opts.useProduceBlockV3 ? this.produceBlockWrapper : this.produceBlockV2Wrapper; + const blockContents = await produceBlockFn(this.config, slot, randaoReveal, graffiti, { + feeRecipient, strictFeeRecipientCheck, - isBuilderEnabled, builderSelection, }).catch((e: Error) => { this.metrics?.blockProposingErrors.inc({error: "produce"}); @@ -143,7 +146,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, @@ -183,260 +186,109 @@ export class BlockProposingService { ? await this.api.beacon.publishBlindedBlock({ signedBlindedBlock: signedBlock, signedBlindedBlobSidecars: signedBlobSidecars, - } as SignedBlindedBlockContents) - : await this.api.beacon.publishBlock({signedBlock, signedBlobSidecars} as SignedBlockContents) + } as allForks.SignedBlindedBlockContents) + : await this.api.beacon.publishBlock({signedBlock, signedBlobSidecars} as allForks.SignedBlockContents) ); } }; private produceBlockWrapper = async ( + _config: ChainForkConfig, slot: Slot, 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; - blockValue: 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 builderBlockValue = blindedBlock?.blockValue ?? BigInt(0); - const engineBlockValue = fullBlock?.blockValue ?? BigInt(0); - - const feeRecipientCheck = {expectedFeeRecipient, strictFeeRecipientCheck}; - - if (fullBlock && blindedBlock) { - let selectedSource: ProducedBlockSource; - let selectedBlock; - switch (builderSelection) { - case BuilderSelection.MaxProfit: { - // If blockValues are zero, than choose builder as most likely beacon didn't provide blockValues - // and builder blocks are most likely thresholded by a min bid - if (engineBlockValue >= builderBlockValue && engineBlockValue !== 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 - engineBlockValue: `${engineBlockValue}`, - builderBlockValue: `${builderBlockValue}`, - }); - 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 - engineBlockValue: `${engineBlockValue}`, - }); - 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 - builderBlockValue: `${builderBlockValue}`, - }); - return this.getBlockWithDebugLog(blindedBlock, ProducedBlockSource.builder, feeRecipientCheck); - } else { - throw Error("Failed to produce engine or builder block"); - } - }; + {feeRecipient, strictFeeRecipientCheck, builderSelection}: routes.validator.ExtraProduceBlockOps + ): Promise => { + const res = await this.api.validator.produceBlockV3(slot, randaoReveal, graffiti, false, { + feeRecipient, + builderSelection, + strictFeeRecipientCheck, + }); + ApiError.assert(res, "Failed to produce block: validator.produceBlockV2"); + const {response} = res; - private getBlockWithDebugLog( - fullOrBlindedBlock: { - block: allForks.FullOrBlindedBeaconBlock; - blockValue: 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 - blockValue: `${formatBigDecimal(fullOrBlindedBlock.blockValue, 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, + builderSelection, + api: "produceBlockV3", }; - 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}; - } + return parseProduceBlockResponse(response, debugLogCtx); + }; - /** Wrapper around the API's different methods for producing blocks across forks */ - private produceBlock = async ( + /** a wrapper function used for backward compatibility with the clients who don't have v3 implemented yet */ + private produceBlockV2Wrapper = async ( + config: ChainForkConfig, slot: Slot, randaoReveal: BLSSignature, graffiti: string, - expectedFeeRecipient?: string - ): Promise<{block: allForks.BeaconBlock; blobs?: deneb.BlobSidecars; blockValue: 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, blockValue} = res.response; - return {block, blockValue}; - } - - // 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, blockValue} = response as {data: allForks.BeaconBlock; blockValue: Wei}; - return {block, blockValue}; - } - - case ForkName.deneb: - default: { - const res = await this.api.validator.produceBlockV2(slot, randaoReveal, graffiti, expectedFeeRecipient); - ApiError.assert(res, "Failed to produce block: validator.produceBlockV2"); + {builderSelection}: routes.validator.ExtraProduceBlockOps + ): 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 + const debugLogCtx = {builderSelection}; + const fork = config.getForkName(slot); + + if (ForkSeq[fork] < ForkSeq.bellatrix || builderSelection === routes.validator.BuilderSelection.ExecutionOnly) { + Object.assign(debugLogCtx, {api: "produceBlockV2"}); + 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); + } 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 {response} = res; - if (!isBlockContents(response.data)) { - throw Error(`Expected BlockContents response at fork=${fork}`); - } - const { - data: {block, blobSidecars: blobs}, - blockValue, - } = response as {data: BlockContents; blockValue: Wei}; - return {block, blobs, blockValue}; - } + return parseProduceBlockResponse({executionPayloadBlinded: true, ...response}, debugLogCtx); } }; +} - private produceBlindedBlock = async ( - slot: Slot, - randaoReveal: BLSSignature, - graffiti: string - ): Promise<{block: allForks.BlindedBeaconBlock; blockValue: 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, blockValue} = response as {data: allForks.BlindedBeaconBlock; blockValue: Wei}; - return {block, blockValue}; - } - - case ForkName.deneb: - default: { - if (!isBlindedBlockContents(response.data)) { - throw Error(`Expected BlockContents response at fork=${fork}`); - } - const { - data: {blindedBlock: block, blindedBlobSidecars: blobs}, - blockValue, - } = response as {data: BlindedBlockContents; blockValue: Wei}; - return {block, blobs, blockValue}; - } +function parseProduceBlockResponse( + response: routes.validator.ProduceFullOrBlindedBlockOrContentsRes, + debugLogCtx: Record +): FullOrBlindedBlockWithContents & DebugLogCtx { + if (response.executionPayloadBlinded) { + if (isBlindedBlockContents(response.data)) { + return { + block: response.data.blindedBlock, + blobs: response.data.blindedBlobSidecars, + version: response.version, + executionPayloadBlinded: true, + debugLogCtx, + } as FullOrBlindedBlockWithContents & DebugLogCtx; + } else { + return { + block: response.data, + blobs: null, + version: response.version, + executionPayloadBlinded: true, + debugLogCtx, + } as FullOrBlindedBlockWithContents & DebugLogCtx; } - }; + } else { + if (isBlockContents(response.data)) { + return { + block: response.data.block, + blobs: response.data.blobSidecars, + version: response.version, + executionPayloadBlinded: false, + debugLogCtx, + } as FullOrBlindedBlockWithContents & DebugLogCtx; + } else { + return { + block: response.data, + blobs: null, + version: response.version, + executionPayloadBlinded: false, + debugLogCtx, + } as FullOrBlindedBlockWithContents & DebugLogCtx; + } + } } diff --git a/packages/validator/src/services/prepareBeaconProposer.ts b/packages/validator/src/services/prepareBeaconProposer.ts index e474614a7a1d..7ca939fb0c41 100644 --- a/packages/validator/src/services/prepareBeaconProposer.ts +++ b/packages/validator/src/services/prepareBeaconProposer.ts @@ -84,7 +84,9 @@ export function pollBuilderValidatorRegistration( .getAllLocalIndices() .map((index) => validatorStore.getPubkeyOfIndex(index)) .filter( - (pubkeyHex): pubkeyHex is string => pubkeyHex !== undefined && validatorStore.isBuilderEnabled(pubkeyHex) + (pubkeyHex): pubkeyHex is string => + pubkeyHex !== undefined && + validatorStore.getBuilderSelection(pubkeyHex) !== 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 9dbf79b40847..2afbeddbc091 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -63,21 +63,13 @@ export type SignerRemote = { pubkey: PubkeyHex; }; -export enum BuilderSelection { - BuilderAlways = "builderalways", - MaxProfit = "maxprofit", - /** Only activate builder flow for DVT block proposal protocols */ - BuilderOnly = "builderonly", -} - type DefaultProposerConfig = { graffiti: string; strictFeeRecipientCheck: boolean; feeRecipient: Eth1Address; builder: { - enabled: boolean; gasLimit: number; - selection: BuilderSelection; + selection: routes.validator.BuilderSelection; }; }; @@ -86,9 +78,8 @@ export type ProposerConfig = { strictFeeRecipientCheck?: boolean; feeRecipient?: Eth1Address; builder?: { - enabled?: boolean; gasLimit?: number; - selection?: BuilderSelection; + selection?: routes.validator.BuilderSelection; }; }; @@ -131,7 +122,9 @@ type ValidatorData = ProposerConfig & { export const defaultOptions = { suggestedFeeRecipient: "0x0000000000000000000000000000000000000000", defaultGasLimit: 30_000_000, - builderSelection: BuilderSelection.MaxProfit, + builderSelection: routes.validator.BuilderSelection.MaxProfit, + // turn it off by default, turn it back on once other clients support v3 api + useProduceBlockV3: false, }; /** @@ -163,7 +156,6 @@ export class ValidatorStore { strictFeeRecipientCheck: defaultConfig.strictFeeRecipientCheck ?? false, feeRecipient: defaultConfig.feeRecipient ?? defaultOptions.suggestedFeeRecipient, builder: { - enabled: defaultConfig.builder?.enabled ?? false, gasLimit: defaultConfig.builder?.gasLimit ?? defaultOptions.defaultGasLimit, selection: defaultConfig.builder?.selection ?? defaultOptions.builderSelection, }, @@ -240,11 +232,7 @@ export class ValidatorStore { return this.validators.get(pubkeyHex)?.graffiti ?? this.defaultProposerConfig.graffiti; } - isBuilderEnabled(pubkeyHex: PubkeyHex): boolean { - return (this.validators.get(pubkeyHex)?.builder || {}).enabled ?? this.defaultProposerConfig.builder.enabled; - } - - getBuilderSelection(pubkeyHex: PubkeyHex): BuilderSelection { + getBuilderSelection(pubkeyHex: PubkeyHex): routes.validator.BuilderSelection { return (this.validators.get(pubkeyHex)?.builder || {}).selection ?? this.defaultProposerConfig.builder.selection; } @@ -297,7 +285,6 @@ export class ValidatorStore { graffiti !== undefined || strictFeeRecipientCheck !== undefined || feeRecipient !== undefined || - builder?.enabled !== undefined || builder?.gasLimit !== undefined ) { proposerConfig = {graffiti, strictFeeRecipientCheck, feeRecipient, builder}; diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index 6deb7182339b..b9bdc0be742a 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -16,7 +16,7 @@ import {Interchange, InterchangeFormatVersion, ISlashingProtection} from "./slas import {assertEqualParams, getLoggerVc, NotEqualParamsError} from "./util/index.js"; import {ChainHeaderTracker} from "./services/chainHeaderTracker.js"; import {ValidatorEventEmitter} from "./services/emitter.js"; -import {ValidatorStore, Signer, ValidatorProposerConfig} from "./services/validatorStore.js"; +import {ValidatorStore, Signer, ValidatorProposerConfig, defaultOptions} from "./services/validatorStore.js"; import {LodestarValidatorDatabaseController, ProcessShutdownCallback, PubkeyHex} from "./types.js"; import {BeaconHealth, Metrics} from "./metrics.js"; import {MetaDataRepository} from "./repositories/metaDataRepository.js"; @@ -56,6 +56,7 @@ export type ValidatorOptions = { closed?: boolean; valProposerConfig?: ValidatorProposerConfig; distributed?: boolean; + useProduceBlockV3?: boolean; }; // TODO: Extend the timeout, and let it be customizable @@ -205,7 +206,9 @@ export class Validator { const chainHeaderTracker = new ChainHeaderTracker(logger, api, emitter); - const blockProposingService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, metrics); + const blockProposingService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, metrics, { + useProduceBlockV3: opts.useProduceBlockV3 ?? defaultOptions.useProduceBlockV3, + }); const attestationService = new AttestationService( loggerVc, diff --git a/packages/validator/test/unit/services/block.test.ts b/packages/validator/test/unit/services/block.test.ts index 585b2d00e336..ce7fb3465220 100644 --- a/packages/validator/test/unit/services/block.test.ts +++ b/packages/validator/test/unit/services/block.test.ts @@ -7,6 +7,7 @@ import {config as mainnetConfig} from "@lodestar/config/default"; import {sleep} from "@lodestar/utils"; import {ssz} from "@lodestar/types"; import {HttpStatusCode} from "@lodestar/api"; +import {ForkName} from "@lodestar/params"; import {BlockProposingService} from "../../../src/services/block.js"; import {ValidatorStore} from "../../../src/services/validatorStore.js"; import {getApiClientStub} from "../../utils/apiStub.js"; @@ -48,13 +49,21 @@ describe("BlockDutiesService", function () { }); const clock = new ClockMock(); - const blockService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, null); + // use produceBlockV3 + const blockService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, null, { + useProduceBlockV3: true, + }); const signedBlock = ssz.phase0.SignedBeaconBlock.defaultValue(); validatorStore.signRandao.resolves(signedBlock.message.body.randaoReveal); validatorStore.signBlock.callsFake(async (_, block) => ({message: block, signature: signedBlock.signature})); - api.validator.produceBlock.resolves({ - response: {data: signedBlock.message, blockValue: ssz.Wei.defaultValue()}, + api.validator.produceBlockV3.resolves({ + response: { + data: signedBlock.message, + version: ForkName.bellatrix, + executionPayloadValue: BigInt(1), + executionPayloadBlinded: false, + }, ok: true, status: HttpStatusCode.OK, }); diff --git a/packages/validator/test/unit/services/produceBlockwrapper.test.ts b/packages/validator/test/unit/services/produceBlockwrapper.test.ts deleted file mode 100644 index e97d9c6efb99..000000000000 --- a/packages/validator/test/unit/services/produceBlockwrapper.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import {expect} from "chai"; -import sinon from "sinon"; -import {createChainForkConfig} from "@lodestar/config"; -import {config as mainnetConfig} from "@lodestar/config/default"; -import {ssz} from "@lodestar/types"; -import {ForkName} from "@lodestar/params"; -import {HttpStatusCode} from "@lodestar/api"; - -import {BlockProposingService} from "../../../src/services/block.js"; -import {ValidatorStore, BuilderSelection} from "../../../src/services/validatorStore.js"; -import {getApiClientStub} from "../../utils/apiStub.js"; -import {loggerVc} from "../../utils/logger.js"; -import {ClockMock} from "../../utils/clock.js"; - -describe("Produce Block with BuilderSelection", function () { - const sandbox = sinon.createSandbox(); - const api = getApiClientStub(sandbox); - const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & - sinon.SinonStubbedInstance; - - // eslint-disable-next-line @typescript-eslint/naming-convention - const config = createChainForkConfig({...mainnetConfig, ALTAIR_FORK_EPOCH: 0, BELLATRIX_FORK_EPOCH: 0}); - - const clock = new ClockMock(); - const blockService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, null); - const produceBlockWrapper = blockService["produceBlockWrapper"]; - - const blindedBlock = ssz.bellatrix.BlindedBeaconBlock.defaultValue(); - const fullBlock = ssz.bellatrix.BeaconBlock.defaultValue(); - const randaoReveal = fullBlock.body.randaoReveal; - - let controller: AbortController; // To stop clock - beforeEach(() => (controller = new AbortController())); - afterEach(() => controller.abort()); - - // Testcase: BuilderSelection, builderBlockValue, engineBlockValue, selection - // null blockValue means the block was not produced - const testCases: [BuilderSelection, number | null, number | null, string][] = [ - [BuilderSelection.MaxProfit, 1, 0, "builder"], - [BuilderSelection.MaxProfit, 1, 2, "engine"], - [BuilderSelection.MaxProfit, null, 0, "engine"], - [BuilderSelection.MaxProfit, 0, null, "builder"], - - [BuilderSelection.BuilderAlways, 1, 2, "builder"], - [BuilderSelection.BuilderAlways, 1, 0, "builder"], - [BuilderSelection.BuilderAlways, null, 0, "engine"], - [BuilderSelection.BuilderAlways, 0, null, "builder"], - ]; - testCases.forEach(([builderSelection, builderBlockValue, engineBlockValue, finalSelection]) => { - it(`builder selection = ${builderSelection}, builder blockValue = ${builderBlockValue}, engine blockValue = ${engineBlockValue} - expected selection = ${finalSelection} `, async function () { - if (builderBlockValue !== null) { - api.validator.produceBlindedBlock.resolves({ - response: { - data: blindedBlock, - version: ForkName.bellatrix, - blockValue: BigInt(builderBlockValue), - }, - ok: true, - status: HttpStatusCode.OK, - }); - } else { - api.validator.produceBlindedBlock.throws(Error("not produced")); - } - - if (engineBlockValue !== null) { - api.validator.produceBlock.resolves({ - response: {data: fullBlock, blockValue: BigInt(engineBlockValue)}, - ok: true, - status: HttpStatusCode.OK, - }); - api.validator.produceBlockV2.resolves({ - response: { - data: fullBlock, - version: ForkName.bellatrix, - blockValue: BigInt(engineBlockValue), - }, - ok: true, - status: HttpStatusCode.OK, - }); - } else { - api.validator.produceBlock.throws(Error("not produced")); - api.validator.produceBlockV2.throws(Error("not produced")); - } - - const produceBlockOpts = { - strictFeeRecipientCheck: false, - expectedFeeRecipient: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - isBuilderEnabled: true, - builderSelection, - }; - const { - debugLogCtx: {source}, - } = (await produceBlockWrapper(144897, randaoReveal, "", produceBlockOpts)) as unknown as { - debugLogCtx: {source: string}; - }; - expect(source).to.equal(finalSelection, "blindedBlock must be returned"); - }); - }); -}); diff --git a/packages/validator/test/unit/validatorStore.test.ts b/packages/validator/test/unit/validatorStore.test.ts index c08ce0c61244..37cae9c2b558 100644 --- a/packages/validator/test/unit/validatorStore.test.ts +++ b/packages/validator/test/unit/validatorStore.test.ts @@ -5,6 +5,7 @@ import bls from "@chainsafe/bls"; import {toHexString, fromHexString} from "@chainsafe/ssz"; import {chainConfig} from "@lodestar/config/default"; import {bellatrix} from "@lodestar/types"; +import {routes} from "@lodestar/api"; import {ValidatorStore} from "../../src/services/validatorStore.js"; import {getApiClientStub} from "../utils/apiStub.js"; @@ -29,8 +30,8 @@ describe("ValidatorStore", function () { strictFeeRecipientCheck: true, feeRecipient: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", builder: { - enabled: false, gasLimit: 30000000, + selection: routes.validator.BuilderSelection.ExecutionOnly, }, }, }, @@ -39,7 +40,6 @@ describe("ValidatorStore", function () { strictFeeRecipientCheck: false, feeRecipient: "0xcccccccccccccccccccccccccccccccccccccccc", builder: { - enabled: true, gasLimit: 35000000, }, }, @@ -61,9 +61,6 @@ describe("ValidatorStore", function () { expect(validatorStore.getFeeRecipient(toHexString(pubkeys[0]))).to.be.equal( valProposerConfig.proposerConfig[toHexString(pubkeys[0])].feeRecipient ); - expect(validatorStore.isBuilderEnabled(toHexString(pubkeys[0]))).to.be.equal( - valProposerConfig.proposerConfig[toHexString(pubkeys[0])].builder?.enabled - ); expect(validatorStore.strictFeeRecipientCheck(toHexString(pubkeys[0]))).to.be.equal( valProposerConfig.proposerConfig[toHexString(pubkeys[0])].strictFeeRecipientCheck ); @@ -76,9 +73,6 @@ describe("ValidatorStore", function () { expect(validatorStore.getFeeRecipient(toHexString(pubkeys[1]))).to.be.equal( valProposerConfig.defaultConfig.feeRecipient ); - expect(validatorStore.isBuilderEnabled(toHexString(pubkeys[1]))).to.be.equal( - valProposerConfig.defaultConfig.builder?.enabled - ); expect(validatorStore.strictFeeRecipientCheck(toHexString(pubkeys[1]))).to.be.equal( valProposerConfig.defaultConfig.strictFeeRecipientCheck );