Skip to content

Commit

Permalink
feat: enable builder proposals post deneb with blobs (#5933)
Browse files Browse the repository at this point in the history
implement missing blindedblock publishing

remove the throw

refactor the type reconstructions for builder

improv
  • Loading branch information
g11tech authored Oct 30, 2023
1 parent 2b5935a commit 8dbef3f
Show file tree
Hide file tree
Showing 16 changed files with 278 additions and 127 deletions.
22 changes: 15 additions & 7 deletions packages/api/src/builder/routes.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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
>
>;
Expand Down Expand Up @@ -84,8 +87,13 @@ export function getReturnTypes(): ReturnTypes<Api> {
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<allForks.ExecutionPayload | allForks.ExecutionPayloadAndBlobsBundle>(
(fork: ForkName) =>
isForkBlobs(fork)
? ssz.allForksBlobs[fork].ExecutionPayloadAndBlobsBundle
: isForkExecution(fork)
? ssz.allForksExecution[fork].ExecutionPayload
: ssz.bellatrix.ExecutionPayload
),
};
}
100 changes: 21 additions & 79 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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});
};

Expand Down Expand Up @@ -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<string, LogDataBasic>
): 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<string, LogDataBasic>
signedBlindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents
): Promise<allForks.SignedBeaconBlockOrContents> {
// 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;
}
15 changes: 9 additions & 6 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 25 additions & 1 deletion packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BlockHash, deneb.BlobSidecars>();
readonly producedBlindedBlobSidecarsCache = new Map<BlockHash, deneb.BlindedBlobSidecars>();

// 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
Expand Down Expand Up @@ -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,
Expand All @@ -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};
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface IBeaconChain {
readonly checkpointBalancesCache: CheckpointBalancesCache;
readonly producedBlobSidecarsCache: Map<BlockHash, deneb.BlobSidecars>;
readonly producedBlockRoot: Map<RootHex, allForks.ExecutionPayload | null>;
readonly producedBlindedBlobSidecarsCache: Map<BlockHash, deneb.BlindedBlobSidecars>;
readonly producedBlindedBlockRoot: Set<RootHex>;
readonly opts: IChainOptions;

Expand Down
64 changes: 49 additions & 15 deletions packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<T extends BlockType>(
this: BeaconChain,
Expand Down Expand Up @@ -195,16 +199,47 @@ export async function produceBlockBody<T extends BlockType>(
);
(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};
}
Expand Down Expand Up @@ -270,7 +305,7 @@ export async function produceBlockBody<T extends BlockType>(
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);
}
Expand All @@ -288,6 +323,7 @@ export async function produceBlockBody<T extends BlockType>(
kzgProof: proof,
kzgCommitment: commitment,
};
// Other fields will be injected after postState is calculated
return blobSidecar;
}) as deneb.BlobSidecars;
blobsResult = {type: BlobsResultType.produced, blobSidecars, blockHash};
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 8dbef3f

Please sign in to comment.