diff --git a/packages/api/src/builder/routes.ts b/packages/api/src/builder/routes.ts index dcff20705c1..0136f1deeac 100644 --- a/packages/api/src/builder/routes.ts +++ b/packages/api/src/builder/routes.ts @@ -1,6 +1,6 @@ import {fromHexString, toHexString} from "@chainsafe/ssz"; import {ssz, allForks, bellatrix, Slot, Root, BLSPubkey} from "@lodestar/types"; -import {ForkName, isForkExecution} from "@lodestar/params"; +import {ForkName, isForkExecution, isForkBlobs} from "@lodestar/params"; import {ChainForkConfig} from "@lodestar/config"; import { @@ -34,11 +34,14 @@ export type Api = { HttpStatusCode.NOT_FOUND | HttpStatusCode.BAD_REQUEST > >; - submitBlindedBlock( - signedBlock: allForks.SignedBlindedBeaconBlockOrContents - ): Promise< + submitBlindedBlock(signedBlock: allForks.SignedBlindedBeaconBlockOrContents): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: allForks.ExecutionPayload; version: ForkName}}, + { + [HttpStatusCode.OK]: { + data: allForks.ExecutionPayload | allForks.ExecutionPayloadAndBlobsBundle; + version: ForkName; + }; + }, HttpStatusCode.SERVICE_UNAVAILABLE > >; @@ -84,8 +87,13 @@ export function getReturnTypes(): ReturnTypes { getHeader: WithVersion((fork: ForkName) => isForkExecution(fork) ? ssz.allForksExecution[fork].SignedBuilderBid : ssz.bellatrix.SignedBuilderBid ), - submitBlindedBlock: WithVersion((fork: ForkName) => - isForkExecution(fork) ? ssz.allForksExecution[fork].ExecutionPayload : ssz.bellatrix.ExecutionPayload + submitBlindedBlock: WithVersion( + (fork: ForkName) => + isForkBlobs(fork) + ? ssz.allForksBlobs[fork].ExecutionPayloadAndBlobsBundle + : isForkExecution(fork) + ? ssz.allForksExecution[fork].ExecutionPayload + : ssz.bellatrix.ExecutionPayload ), }; } 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 81eac4ed6e4..3b36674dd61 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,13 @@ import {fromHexString, toHexString} from "@chainsafe/ssz"; import {routes, ServerApi, ResponseFormat} from "@lodestar/api"; -import {computeTimeAtSlot, signedBlindedBlockToFull, signedBlindedBlobSidecarsToFull} from "@lodestar/state-transition"; +import { + computeTimeAtSlot, + parseSignedBlindedBlockOrContents, + reconstructFullBlockOrContents, +} from "@lodestar/state-transition"; import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; -import {sleep, toHex, LogDataBasic} from "@lodestar/utils"; -import {allForks, deneb, isSignedBlockContents, isSignedBlindedBlockContents} from "@lodestar/types"; +import {sleep, toHex} from "@lodestar/utils"; +import {allForks, deneb, isSignedBlockContents, ProducedBlockSource} 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,11 +19,6 @@ 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 @@ -152,27 +151,28 @@ export function getBeaconBlockApi({ .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); + // executionPayload can be null or a real payload in locally produced so check for presence of root + const source = chain.producedBlockRoot.has(blockRoot) ? ProducedBlockSource.engine : ProducedBlockSource.builder; + + const executionPayload = chain.producedBlockRoot.get(blockRoot) ?? null; + const blobSidecars = executionPayload + ? chain.producedBlobSidecarsCache.get(toHex(executionPayload.blockHash)) + : undefined; + const blobs = blobSidecars ? blobSidecars.map((blobSidecar) => blobSidecar.blob) : null; + const signedBlockOrContents = - executionPayload !== undefined - ? reconstructLocalBlockOrContents( - chain, - {signedBlindedBlock, signedBlindedBlobSidecars}, - executionPayload, - logCtx - ) - : await reconstructBuilderBlockOrContents(chain, signedBlindedBlockOrContents, logCtx); + source === ProducedBlockSource.engine + ? reconstructFullBlockOrContents({signedBlindedBlock, signedBlindedBlobSidecars}, {executionPayload, blobs}) + : await reconstructBuilderBlockOrContents(chain, 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 + chain.logger.info("Publishing assembled block", {blockRoot, slot, source}); return publishBlock(signedBlockOrContents, {...opts, ignoreIfKnown: true}); }; @@ -365,73 +365,15 @@ 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 + signedBlindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents ): 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 ec3a02dfa26..b14e1b3ade5 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -310,14 +310,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 allForks.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 31068d20de2..18c37a3ba43 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -131,6 +131,7 @@ export class BeaconChain implements IBeaconChain { readonly checkpointBalancesCache: CheckpointBalancesCache; /** Map keyed by executionPayload.blockHash of the block for those blobs */ readonly producedBlobSidecarsCache = new Map(); + readonly producedBlindedBlobSidecarsCache = new Map(); // 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 @@ -522,7 +523,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, @@ -533,6 +534,21 @@ export class BeaconChain implements IBeaconChain { this.producedBlobSidecarsCache.set(blockHash, blobSidecars); this.metrics?.blockProductionCaches.producedBlobSidecarsCache.set(this.producedBlobSidecarsCache.size); + } 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); + this.metrics?.blockProductionCaches.producedBlindedBlobSidecarsCache.set( + this.producedBlindedBlobSidecarsCache.size + ); } return {block, executionPayloadValue}; @@ -792,6 +808,14 @@ export class BeaconChain implements IBeaconChain { this.opts.maxCachedBlobSidecars ?? DEFAULT_MAX_CACHED_BLOB_SIDECARS ); this.metrics?.blockProductionCaches.producedBlobSidecarsCache.set(this.producedBlobSidecarsCache.size); + + pruneSetToMax( + this.producedBlindedBlobSidecarsCache, + this.opts.maxCachedBlobSidecars ?? DEFAULT_MAX_CACHED_BLOB_SIDECARS + ); + this.metrics?.blockProductionCaches.producedBlindedBlobSidecarsCache.set( + this.producedBlindedBlobSidecarsCache.size + ); } const metrics = this.metrics; diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index b7f33555545..8d6f7f419d7 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -95,6 +95,7 @@ export interface IBeaconChain { readonly checkpointBalancesCache: CheckpointBalancesCache; readonly producedBlobSidecarsCache: Map; readonly producedBlockRoot: Map; + readonly producedBlindedBlobSidecarsCache: Map; 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 5224aae6503..acefbbf765a 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 54e90672d18..0d00d0c8bd7 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 9a423e0f832..bfe003372ce 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -1,8 +1,13 @@ import {byteArrayEquals, toHexString} from "@chainsafe/ssz"; import {allForks, bellatrix, Slot, Root, BLSPubkey, ssz, deneb, Wei} from "@lodestar/types"; +import { + parseSignedBlindedBlockOrContents, + parseExecutionPayloadAndBlobsBundle, + reconstructFullBlockOrContents, +} from "@lodestar/state-transition"; 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"; @@ -91,27 +96,36 @@ 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 { - const res = await this.api.submitBlindedBlock(signedBlock); + async submitBlindedBlock( + signedBlindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents + ): Promise { + const res = await this.api.submitBlindedBlock(signedBlindedBlockOrContents); ApiError.assert(res, "execution.builder.submitBlindedBlock"); - const executionPayload = res.response.data; - const expectedTransactionsRoot = signedBlock.message.body.executionPayloadHeader.transactionsRoot; - const actualTransactionsRoot = ssz.bellatrix.Transactions.hashTreeRoot(res.response.data.transactions); + const {data} = res.response; + + const {executionPayload, blobsBundle} = parseExecutionPayloadAndBlobsBundle(data); + const {signedBlindedBlock, signedBlindedBlobSidecars} = + parseSignedBlindedBlockOrContents(signedBlindedBlockOrContents); + + // some validations for execution payload + const expectedTransactionsRoot = signedBlindedBlock.message.body.executionPayloadHeader.transactionsRoot; + const actualTransactionsRoot = ssz.bellatrix.Transactions.hashTreeRoot(executionPayload.transactions); if (!byteArrayEquals(expectedTransactionsRoot, actualTransactionsRoot)) { throw Error( `Invalid transactionsRoot of the builder payload, expected=${toHexString( @@ -119,10 +133,8 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { )}, actual=${toHexString(actualTransactionsRoot)}` ); } - const fullySignedBlock: bellatrix.SignedBeaconBlock = { - ...signedBlock, - message: {...signedBlock.message, body: {...signedBlock.message.body, executionPayload}}, - }; - return fullySignedBlock; + + const blobs = blobsBundle ? blobsBundle.blobs : null; + return reconstructFullBlockOrContents({signedBlindedBlock, signedBlindedBlobSidecars}, {executionPayload, blobs}); } } diff --git a/packages/beacon-node/src/execution/builder/interface.ts b/packages/beacon-node/src/execution/builder/interface.ts index 2bc7a19765a..e9a2cabb69e 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,16 @@ 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; + submitBlindedBlock( + signedBlock: allForks.SignedBlindedBeaconBlockOrContents + ): Promise; } diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index 9ea233b18fa..2b763599f6e 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -151,6 +151,10 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { name: "beacon_blobsidecars_produced_cache_total", help: "Count of cached produced blob sidecars", }), + producedBlindedBlobSidecarsCache: register.gauge({ + name: "beacon_blinded_blobsidecars_produced_cache_total", + help: "Count of cached produced blinded blob sidecars", + }), }, blockPayload: { diff --git a/packages/state-transition/src/util/blindedBlock.ts b/packages/state-transition/src/util/blindedBlock.ts index 02f0397a33e..8c271e7fec8 100644 --- a/packages/state-transition/src/util/blindedBlock.ts +++ b/packages/state-transition/src/util/blindedBlock.ts @@ -1,9 +1,24 @@ import {ChainForkConfig} from "@lodestar/config"; import {ForkSeq} from "@lodestar/params"; -import {allForks, phase0, Root, isBlindedBeaconBlock, isBlindedBlobSidecar, deneb, ssz} from "@lodestar/types"; +import { + allForks, + phase0, + Root, + deneb, + ssz, + isBlindedBeaconBlock, + isBlindedBlobSidecar, + isSignedBlindedBlockContents, + isExecutionPayloadAndBlobsBundle, +} from "@lodestar/types"; import {executionPayloadToPayloadHeader} from "./execution.js"; +type ParsedSignedBlindedBlockOrContents = { + signedBlindedBlock: allForks.SignedBlindedBeaconBlock; + signedBlindedBlobSidecars: deneb.SignedBlindedBlobSidecars | null; +}; + export function blindedOrFullBlockHashTreeRoot( config: ChainForkConfig, blindedOrFull: allForks.FullOrBlindedBeaconBlock @@ -99,3 +114,55 @@ export function signedBlindedBlobSidecarsToFull( }); return signedBlobSidecars; } + +export 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}; + } +} + +export function parseExecutionPayloadAndBlobsBundle( + data: allForks.ExecutionPayload | allForks.ExecutionPayloadAndBlobsBundle +): {executionPayload: allForks.ExecutionPayload; blobsBundle: deneb.BlobsBundle | null} { + if (isExecutionPayloadAndBlobsBundle(data)) { + return data; + } else { + return { + executionPayload: data, + blobsBundle: null, + }; + } +} + +export function reconstructFullBlockOrContents( + {signedBlindedBlock, signedBlindedBlobSidecars}: ParsedSignedBlindedBlockOrContents, + {executionPayload, blobs}: {executionPayload: allForks.ExecutionPayload | null; blobs: deneb.Blobs | null} +): allForks.SignedBeaconBlockOrContents { + const signedBlock = signedBlindedBlockToFull(signedBlindedBlock, executionPayload); + + if (signedBlindedBlobSidecars !== null) { + if (executionPayload === null) { + throw Error("Missing locally produced executionPayload for deneb+ publishBlindedBlock"); + } + + if (blobs === null) { + throw Error("Missing blobs from the local execution cache"); + } + if (blobs.length !== signedBlindedBlobSidecars.length) { + throw Error( + `Length mismatch signedBlindedBlobSidecars=${signedBlindedBlobSidecars.length} blobs=${blobs.length}` + ); + } + const signedBlobSidecars = signedBlindedBlobSidecarsToFull(signedBlindedBlobSidecars, blobs); + + return {signedBlock, signedBlobSidecars} as allForks.SignedBeaconBlockOrContents; + } else { + return signedBlock as allForks.SignedBeaconBlockOrContents; + } +} diff --git a/packages/types/src/allForks/sszTypes.ts b/packages/types/src/allForks/sszTypes.ts index 023d7bc8636..463e5c57bd0 100644 --- a/packages/types/src/allForks/sszTypes.ts +++ b/packages/types/src/allForks/sszTypes.ts @@ -156,5 +156,6 @@ export const allForksBlobs = { deneb: { BlobSidecar: deneb.BlobSidecar, BlindedBlobSidecar: deneb.BlindedBlobSidecar, + ExecutionPayloadAndBlobsBundle: deneb.ExecutionPayloadAndBlobsBundle, }, }; diff --git a/packages/types/src/allForks/types.ts b/packages/types/src/allForks/types.ts index a525820aac0..01c597b8a24 100644 --- a/packages/types/src/allForks/types.ts +++ b/packages/types/src/allForks/types.ts @@ -96,6 +96,7 @@ export type SignedBlindedBeaconBlockOrContents = SignedBlindedBeaconBlock | Sign export type BuilderBid = bellatrix.BuilderBid | capella.BuilderBid | deneb.BuilderBid; export type SignedBuilderBid = bellatrix.SignedBuilderBid | capella.SignedBuilderBid | deneb.SignedBuilderBid; +export type ExecutionPayloadAndBlobsBundle = deneb.ExecutionPayloadAndBlobsBundle; export type LightClientHeader = altair.LightClientHeader | capella.LightClientHeader | deneb.LightClientHeader; export type LightClientBootstrap = @@ -308,4 +309,5 @@ export type AllForksLightClientSSZTypes = { export type AllForksBlobsSSZTypes = { BlobSidecar: AllForksTypeOf; BlindedBlobSidecar: AllForksTypeOf; + ExecutionPayloadAndBlobsBundle: AllForksTypeOf; }; diff --git a/packages/types/src/deneb/sszTypes.ts b/packages/types/src/deneb/sszTypes.ts index a527cf3b4f4..96509d1d898 100644 --- a/packages/types/src/deneb/sszTypes.ts +++ b/packages/types/src/deneb/sszTypes.ts @@ -149,6 +149,15 @@ export const SignedBlobSidecar = new ContainerType( ); export const SignedBlobSidecars = new ListCompositeType(SignedBlobSidecar, MAX_BLOB_COMMITMENTS_PER_BLOCK); +export const BlobsBundle = new ContainerType( + { + commitments: BlobKzgCommitments, + proofs: KZGProofs, + blobs: Blobs, + }, + {typeName: "BlobsBundle", jsonCase: "eth2"} +); + export const BlindedBlobSidecar = new ContainerType( { blockRoot: Root, @@ -204,12 +213,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"} ); @@ -222,6 +240,14 @@ export const SignedBuilderBid = new ContainerType( {typeName: "SignedBuilderBid", jsonCase: "eth2"} ); +export const ExecutionPayloadAndBlobsBundle = new ContainerType( + { + executionPayload: ExecutionPayload, + blobsBundle: BlobsBundle, + }, + {typeName: "ExecutionPayloadAndBlobsBundle", jsonCase: "eth2"} +); + // We don't spread capella.BeaconState fields since we need to replace // latestExecutionPayloadHeader and we cannot keep order doing that export const BeaconState = new ContainerType( diff --git a/packages/types/src/deneb/types.ts b/packages/types/src/deneb/types.ts index 93ea514aea7..1d6eb5fca5a 100644 --- a/packages/types/src/deneb/types.ts +++ b/packages/types/src/deneb/types.ts @@ -16,6 +16,8 @@ export type SignedBlobSidecar = ValueOf; export type SignedBlobSidecars = ValueOf; export type SignedBlindedBlobSidecar = ValueOf; export type SignedBlindedBlobSidecars = ValueOf; +export type ExecutionPayloadAndBlobsBundle = ValueOf; +export type BlobsBundle = ValueOf; export type BlobKzgCommitments = ValueOf; export type KZGProofs = ValueOf; @@ -40,6 +42,7 @@ export type SignedBlindedBeaconBlock = ValueOf; export type BuilderBid = ValueOf; export type SignedBuilderBid = ValueOf; export type SSEPayloadAttributes = ValueOf; diff --git a/packages/types/src/utils/typeguards.ts b/packages/types/src/utils/typeguards.ts index a3e4393c51c..0b9bee97d17 100644 --- a/packages/types/src/utils/typeguards.ts +++ b/packages/types/src/utils/typeguards.ts @@ -16,6 +16,8 @@ import { SignedBlockContents, SignedBeaconBlock, SignedBlindedBeaconBlockOrContents, + ExecutionPayload, + ExecutionPayloadAndBlobsBundle, } from "../allForks/types.js"; import {ts as deneb} from "../deneb/index.js"; @@ -67,3 +69,9 @@ export function isSignedBlindedBlockContents( ): data is SignedBlindedBlockContents { return (data as SignedBlindedBlockContents).signedBlindedBlobSidecars !== undefined; } + +export function isExecutionPayloadAndBlobsBundle( + data: ExecutionPayload | ExecutionPayloadAndBlobsBundle +): data is ExecutionPayloadAndBlobsBundle { + return (data as ExecutionPayloadAndBlobsBundle).blobsBundle !== undefined; +}