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 4833e576f5cc..7b80ca566c15 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,29 @@ 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 + const logCtx = {blockRoot, slot, source: executionPayload !== undefined ? "execution" : "builder"}; + chain.logger.info("Publishing assembled block", logCtx); return publishBlock(signedBlockOrContents, {...opts, ignoreIfKnown: true}); }; @@ -365,62 +366,9 @@ 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 { const executionBuilder = chain.executionBuilder; if (!executionBuilder) { @@ -428,6 +376,5 @@ async function reconstructBuilderBlockOrContents( } 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/execution/builder/http.ts b/packages/beacon-node/src/execution/builder/http.ts index 5ee9c271facb..bfe003372ced 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -1,16 +1,10 @@ import {byteArrayEquals, toHexString} from "@chainsafe/ssz"; +import {allForks, bellatrix, Slot, Root, BLSPubkey, ssz, deneb, Wei} from "@lodestar/types"; import { - allForks, - bellatrix, - Slot, - Root, - BLSPubkey, - ssz, - deneb, - Wei, - isExecutionPayloadAndBlobsBundle, - isSignedBlindedBlockContents, -} from "@lodestar/types"; + 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, ForkExecution} from "@lodestar/params"; @@ -125,26 +119,9 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { ApiError.assert(res, "execution.builder.submitBlindedBlock"); const {data} = res.response; - let executionPayload: allForks.ExecutionPayload; - let blobsBundle: deneb.BlobsBundle | null; - - if (isExecutionPayloadAndBlobsBundle(data)) { - executionPayload = data.executionPayload; - blobsBundle = data.blobsBundle; - } else { - executionPayload = data; - blobsBundle = null; - } - - let signedBlindedBlock: allForks.SignedBlindedBeaconBlock; - let signedBlindedBlobSidecars: deneb.SignedBlindedBlobSidecars | null; - if (isSignedBlindedBlockContents(signedBlindedBlockOrContents)) { - signedBlindedBlock = signedBlindedBlockOrContents.signedBlindedBlock; - signedBlindedBlobSidecars = signedBlindedBlockOrContents.signedBlindedBlobSidecars; - } else { - signedBlindedBlock = signedBlindedBlockOrContents; - signedBlindedBlobSidecars = null; - } + const {executionPayload, blobsBundle} = parseExecutionPayloadAndBlobsBundle(data); + const {signedBlindedBlock, signedBlindedBlobSidecars} = + parseSignedBlindedBlockOrContents(signedBlindedBlockOrContents); // some validations for execution payload const expectedTransactionsRoot = signedBlindedBlock.message.body.executionPayloadHeader.transactionsRoot; @@ -157,37 +134,7 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { ); } - const signedBlock: bellatrix.SignedBeaconBlock = { - ...signedBlindedBlock, - message: {...signedBlindedBlock.message, body: {...signedBlindedBlock.message.body, executionPayload}}, - }; - - if (signedBlindedBlobSidecars !== null) { - if (blobsBundle === null) { - throw Error("Invalid Builder response with missing blobsBundle for deneb+ forks"); - } - if (signedBlindedBlobSidecars.length !== blobsBundle.blobs.length) { - throw Error( - `Invalid number of blobs returned by builder, expected=$${signedBlindedBlobSidecars.length} received=${blobsBundle.blobs.length}` - ); - } - const signedBlobSidecars = signedBlindedBlobSidecars.map((_v, i) => { - // signedBlindedBlobSidecars and blobsBundle can't be null as we checked above but - // typescript can't seem to figure that out - if (signedBlindedBlobSidecars === null || blobsBundle === null) { - throw Error("Internal Error - signedBlindedBlobSidecars or blobsBundle is null"); - } - - const signedBlindedBlobSidecar = signedBlindedBlobSidecars[i]; - const blob = blobsBundle.blobs[i]; - return {signature: signedBlindedBlobSidecar.signature, message: {...signedBlindedBlobSidecar.message, blob}}; - }); - return {signedBlock, signedBlobSidecars}; - } else { - if (blobsBundle !== null) { - throw Error("Invalid Builder response with blobsBundle for deneb- forks"); - } - return signedBlock; - } + const blobs = blobsBundle ? blobsBundle.blobs : null; + return reconstructFullBlockOrContents({signedBlindedBlock, signedBlindedBlobSidecars}, {executionPayload, blobs}); } } diff --git a/packages/state-transition/src/util/blindedBlock.ts b/packages/state-transition/src/util/blindedBlock.ts index 02f0397a33e7..8c271e7fec81 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; + } +}