diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 84ece5c4363e..d5aeabe0efdb 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, isBlindedBlockContents} from "@lodestar/api"; +import {routes, ServerApi, BlockContents, BlindedBlockContents} from "@lodestar/api"; import { CachedBeaconStateAllForks, computeStartSlotAtEpoch, @@ -305,14 +305,17 @@ export function getValidatorApi({ const version = config.getForkName(block.slot); if (isForkBlobs(version)) { - if (!isBlindedBlockContents(block)) { - throw Error(`Expected BlockContents response at fork=${version}`); + const blockHash = toHex((block as bellatrix.BlindedBeaconBlock).body.executionPayloadHeader.blockHash); + const {blindedBlobSidecars} = chain.producedBlindedBlobSidecarsCache.get(blockHash) ?? {}; + if (blindedBlobSidecars === undefined) { + throw Error("blobSidecars missing in cache"); } - return {data: block, version, executionPayloadValue}; + return { + data: {blindedBlock: block, blindedBlobSidecars} as BlindedBlockContents, + version, + executionPayloadValue, + }; } else { - if (isBlindedBlockContents(block)) { - throw Error(`Invalid BlockContents response at fork=${version}`); - } return {data: block, version, executionPayloadValue}; } } finally { diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index d7a394741e63..11b442c511b6 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -13,19 +13,7 @@ import { PubkeyIndexMap, } from "@lodestar/state-transition"; import {BeaconConfig} from "@lodestar/config"; -import { - allForks, - UintNum64, - Root, - phase0, - Slot, - RootHex, - Epoch, - ValidatorIndex, - deneb, - Wei, - bellatrix, -} from "@lodestar/types"; +import {allForks, UintNum64, Root, phase0, Slot, RootHex, Epoch, ValidatorIndex, deneb, Wei} from "@lodestar/types"; import {CheckpointWithHex, ExecutionStatus, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {Logger, isErrorAborted, pruneSetToMax, sleep, toHex} from "@lodestar/utils"; @@ -135,7 +123,7 @@ export class BeaconChain implements IBeaconChain { readonly producedBlobSidecarsCache = new Map(); readonly producedBlindedBlobSidecarsCache = new Map< BlockHash, - {blobSidecars: deneb.BlindedBlobSidecars; slot: Slot} + {blindedBlobSidecars: deneb.BlindedBlobSidecars; slot: Slot} >(); readonly producedBlockRoot = new Set(); @@ -521,7 +509,7 @@ export class BeaconChain implements IBeaconChain { // publishing the blinded block's full version if (blobs.type === BlobsResultType.produced) { // body is of full type here - const blockHash = toHex((block as bellatrix.BeaconBlock).body.executionPayload.blockHash); + const blockHash = blobs.blockHash; const blobSidecars = blobs.blobSidecars.map((blobSidecar) => ({ ...blobSidecar, blockRoot, @@ -535,6 +523,22 @@ export class BeaconChain implements IBeaconChain { this.producedBlobSidecarsCache, this.opts.maxCachedBlobSidecars ?? DEFAULT_MAX_CACHED_BLOB_SIDECARS ); + } else if (blobs.type === BlobsResultType.blinded) { + // body is of blinded type here + const blockHash = blobs.blockHash; + const blindedBlobSidecars = blobs.blobSidecars.map((blindedBlobSidecar) => ({ + ...blindedBlobSidecar, + blockRoot, + slot, + blockParentRoot: parentBlockRoot, + proposerIndex, + })); + + this.producedBlindedBlobSidecarsCache.set(blockHash, {blindedBlobSidecars, slot}); + pruneSetToMax( + this.producedBlindedBlobSidecarsCache, + this.opts.maxCachedBlobSidecars ?? DEFAULT_MAX_CACHED_BLOB_SIDECARS + ); } return {block, executionPayloadValue}; diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 513176972616..688328a4e853 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -94,7 +94,10 @@ export interface IBeaconChain { readonly beaconProposerCache: BeaconProposerCache; readonly checkpointBalancesCache: CheckpointBalancesCache; readonly producedBlobSidecarsCache: Map; - readonly producedBlindedBlobSidecarsCache: Map; + readonly producedBlindedBlobSidecarsCache: Map< + BlockHash, + {blindedBlobSidecars: deneb.BlindedBlobSidecars; slot: Slot} + >; readonly producedBlockRoot: Set; readonly producedBlindedBlockRoot: Set; readonly opts: IChainOptions; diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 5224aae65035..acefbbf765a1 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -35,7 +35,10 @@ import {PayloadId, IExecutionEngine, IExecutionBuilder, PayloadAttributes} from import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; import {IEth1ForBlockProduction} from "../../eth1/index.js"; import {numToQuantity} from "../../eth1/provider/utils.js"; -import {validateBlobsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js"; +import { + validateBlobsAndKzgCommitments, + validateBlindedBlobsAndKzgCommitments, +} from "./validateBlobsAndKzgCommitments.js"; // Time to provide the EL to generate a payload from new payload id const PAYLOAD_GENERATION_TIME_MS = 500; @@ -70,8 +73,9 @@ export enum BlobsResultType { } export type BlobsResult = - | {type: BlobsResultType.preDeneb | BlobsResultType.blinded} - | {type: BlobsResultType.produced; blobSidecars: deneb.BlobSidecars; blockHash: RootHex}; + | {type: BlobsResultType.preDeneb} + | {type: BlobsResultType.produced; blobSidecars: deneb.BlobSidecars; blockHash: RootHex} + | {type: BlobsResultType.blinded; blobSidecars: deneb.BlindedBlobSidecars; blockHash: RootHex}; export async function produceBlockBody( this: BeaconChain, @@ -195,16 +199,47 @@ export async function produceBlockBody( ); (blockBody as allForks.BlindedBeaconBlockBody).executionPayloadHeader = builderRes.header; executionPayloadValue = builderRes.executionPayloadValue; - this.logger.verbose("Fetched execution payload header from builder", {slot: blockSlot, executionPayloadValue}); + + const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime); + const prepType = "blinded"; + this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime); + this.logger.verbose("Fetched execution payload header from builder", { + slot: blockSlot, + executionPayloadValue, + prepType, + fetchedTime, + }); + if (ForkSeq[fork] >= ForkSeq.deneb) { - const {blobKzgCommitments} = builderRes; - if (blobKzgCommitments === undefined) { - throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`); + const {blindedBlobsBundle} = builderRes; + if (blindedBlobsBundle === undefined) { + throw Error(`Invalid builder getHeader response for fork=${fork}, missing blindedBlobsBundle`); } - (blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments; - blobsResult = {type: BlobsResultType.blinded}; - Object.assign(logMeta, {blobs: blobKzgCommitments.length}); + // validate blindedBlobsBundle + if (this.opts.sanityCheckExecutionEngineBlobs) { + validateBlindedBlobsAndKzgCommitments(builderRes.header, blindedBlobsBundle); + } + + (blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blindedBlobsBundle.commitments; + const blockHash = toHex(builderRes.header.blockHash); + + const blobSidecars = Array.from({length: blindedBlobsBundle.blobRoots.length}, (_v, index) => { + const blobRoot = blindedBlobsBundle.blobRoots[index]; + const commitment = blindedBlobsBundle.commitments[index]; + const proof = blindedBlobsBundle.proofs[index]; + const blindedBlobSidecar = { + index, + blobRoot, + kzgProof: proof, + kzgCommitment: commitment, + }; + // Other fields will be injected after postState is calculated + return blindedBlobSidecar; + }) as deneb.BlindedBlobSidecars; + blobsResult = {type: BlobsResultType.blinded, blobSidecars, blockHash}; + + Object.assign(logMeta, {blobs: blindedBlobsBundle.commitments.length}); } else { blobsResult = {type: BlobsResultType.preDeneb}; } @@ -270,7 +305,7 @@ export async function produceBlockBody( throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`); } - // Optionally sanity-check that the KZG commitments match the versioned hashes in the transactions + // validate blindedBlobsBundle if (this.opts.sanityCheckExecutionEngineBlobs) { validateBlobsAndKzgCommitments(executionPayload, blobsBundle); } @@ -288,6 +323,7 @@ export async function produceBlockBody( kzgProof: proof, kzgCommitment: commitment, }; + // Other fields will be injected after postState is calculated return blobSidecar; }) as deneb.BlobSidecars; blobsResult = {type: BlobsResultType.produced, blobSidecars, blockHash}; @@ -443,21 +479,19 @@ async function prepareExecutionPayloadHeader( ): Promise<{ header: allForks.ExecutionPayloadHeader; executionPayloadValue: Wei; - blobKzgCommitments?: deneb.BlobKzgCommitments; + blindedBlobsBundle?: deneb.BlindedBlobsBundle; }> { if (!chain.executionBuilder) { throw Error("executionBuilder required"); } const parentHashRes = await getExecutionPayloadParentHash(chain, state); - if (parentHashRes.isPremerge) { - // TODO: Is this okay? throw Error("Execution builder disabled pre-merge"); } const {parentHash} = parentHashRes; - return chain.executionBuilder.getHeader(state.slot, parentHash, proposerPubKey); + return chain.executionBuilder.getHeader(fork, state.slot, parentHash, proposerPubKey); } export async function getExecutionPayloadParentHash( diff --git a/packages/beacon-node/src/chain/produceBlock/validateBlobsAndKzgCommitments.ts b/packages/beacon-node/src/chain/produceBlock/validateBlobsAndKzgCommitments.ts index 54e90672d189..0d00d0c8bd72 100644 --- a/packages/beacon-node/src/chain/produceBlock/validateBlobsAndKzgCommitments.ts +++ b/packages/beacon-node/src/chain/produceBlock/validateBlobsAndKzgCommitments.ts @@ -1,4 +1,4 @@ -import {allForks} from "@lodestar/types"; +import {allForks, deneb} from "@lodestar/types"; import {BlobsBundle} from "../../execution/index.js"; /** @@ -13,3 +13,15 @@ export function validateBlobsAndKzgCommitments(payload: allForks.ExecutionPayloa ); } } + +export function validateBlindedBlobsAndKzgCommitments( + payload: allForks.ExecutionPayloadHeader, + blindedBlobsBundle: deneb.BlindedBlobsBundle +): void { + // sanity-check that the KZG commitments match the blobs (as produced by the execution engine) + if (blindedBlobsBundle.blobRoots.length !== blindedBlobsBundle.commitments.length) { + throw Error( + `BlindedBlobs bundle blobs len ${blindedBlobsBundle.blobRoots.length} != commitments len ${blindedBlobsBundle.commitments.length}` + ); + } +} diff --git a/packages/beacon-node/src/execution/builder/http.ts b/packages/beacon-node/src/execution/builder/http.ts index 48d8277f2c61..c497ac3dc4bf 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -2,7 +2,7 @@ import {byteArrayEquals, toHexString} from "@chainsafe/ssz"; import {allForks, bellatrix, Slot, Root, BLSPubkey, ssz, deneb, Wei} from "@lodestar/types"; import {ChainForkConfig} from "@lodestar/config"; import {getClient, Api as BuilderApi} from "@lodestar/api/builder"; -import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {SLOTS_PER_EPOCH, ForkExecution} from "@lodestar/params"; import {ApiError} from "@lodestar/api"; import {Metrics} from "../../metrics/metrics.js"; @@ -88,19 +88,20 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { } async getHeader( + fork: ForkExecution, slot: Slot, parentHash: Root, proposerPubKey: BLSPubkey ): Promise<{ header: allForks.ExecutionPayloadHeader; executionPayloadValue: Wei; - blobKzgCommitments?: deneb.BlobKzgCommitments; + blindedBlobsBundle?: deneb.BlindedBlobsBundle; }> { const res = await this.api.getHeader(slot, parentHash, proposerPubKey); ApiError.assert(res, "execution.builder.getheader"); const {header, value: executionPayloadValue} = res.response.data.message; - const {blobKzgCommitments} = res.response.data.message as {blobKzgCommitments?: deneb.BlobKzgCommitments}; - return {header, executionPayloadValue, blobKzgCommitments}; + const {blindedBlobsBundle} = res.response.data.message as deneb.BuilderBid; + return {header, executionPayloadValue, blindedBlobsBundle}; } 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 2bc7a19765a0..67f9f5de1fc4 100644 --- a/packages/beacon-node/src/execution/builder/interface.ts +++ b/packages/beacon-node/src/execution/builder/interface.ts @@ -1,4 +1,5 @@ import {allForks, bellatrix, Root, Slot, BLSPubkey, deneb, Wei} from "@lodestar/types"; +import {ForkExecution} from "@lodestar/params"; export interface IExecutionBuilder { /** @@ -17,13 +18,14 @@ export interface IExecutionBuilder { checkStatus(): Promise; registerValidator(registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise; getHeader( + fork: ForkExecution, slot: Slot, parentHash: Root, proposerPubKey: BLSPubkey ): Promise<{ header: allForks.ExecutionPayloadHeader; executionPayloadValue: Wei; - blobKzgCommitments?: deneb.BlobKzgCommitments; + blindedBlobsBundle?: deneb.BlindedBlobsBundle; }>; submitBlindedBlock(signedBlock: allForks.SignedBlindedBeaconBlock): Promise; } diff --git a/packages/types/src/deneb/sszTypes.ts b/packages/types/src/deneb/sszTypes.ts index a527cf3b4f48..a2e4d7812f00 100644 --- a/packages/types/src/deneb/sszTypes.ts +++ b/packages/types/src/deneb/sszTypes.ts @@ -204,12 +204,21 @@ export const SignedBlindedBeaconBlock = new ContainerType( {typeName: "SignedBlindedBeaconBlock", jsonCase: "eth2"} ); +export const BlindedBlobsBundle = new ContainerType( + { + commitments: BlobKzgCommitments, + proofs: KZGProofs, + blobRoots: BlindedBlobs, + }, + {typeName: "BlindedBlobsBundle", jsonCase: "eth2"} +); + export const BuilderBid = new ContainerType( { header: ExecutionPayloadHeader, + blindedBlobsBundle: BlindedBlobsBundle, value: UintBn256, pubkey: BLSPubkey, - blobKzgCommitments: BlobKzgCommitments, }, {typeName: "BuilderBid", jsonCase: "eth2"} ); diff --git a/packages/types/src/deneb/types.ts b/packages/types/src/deneb/types.ts index 93ea514aea75..7fa7bbe9e7f7 100644 --- a/packages/types/src/deneb/types.ts +++ b/packages/types/src/deneb/types.ts @@ -40,6 +40,7 @@ export type SignedBlindedBeaconBlock = ValueOf; export type BuilderBid = ValueOf; export type SignedBuilderBid = ValueOf; export type SSEPayloadAttributes = ValueOf;