Skip to content

Commit 8dbef3f

Browse files
authored
feat: enable builder proposals post deneb with blobs (#5933)
implement missing blindedblock publishing remove the throw refactor the type reconstructions for builder improv
1 parent 2b5935a commit 8dbef3f

File tree

16 files changed

+278
-127
lines changed

16 files changed

+278
-127
lines changed

packages/api/src/builder/routes.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {fromHexString, toHexString} from "@chainsafe/ssz";
22
import {ssz, allForks, bellatrix, Slot, Root, BLSPubkey} from "@lodestar/types";
3-
import {ForkName, isForkExecution} from "@lodestar/params";
3+
import {ForkName, isForkExecution, isForkBlobs} from "@lodestar/params";
44
import {ChainForkConfig} from "@lodestar/config";
55

66
import {
@@ -34,11 +34,14 @@ export type Api = {
3434
HttpStatusCode.NOT_FOUND | HttpStatusCode.BAD_REQUEST
3535
>
3636
>;
37-
submitBlindedBlock(
38-
signedBlock: allForks.SignedBlindedBeaconBlockOrContents
39-
): Promise<
37+
submitBlindedBlock(signedBlock: allForks.SignedBlindedBeaconBlockOrContents): Promise<
4038
ApiClientResponse<
41-
{[HttpStatusCode.OK]: {data: allForks.ExecutionPayload; version: ForkName}},
39+
{
40+
[HttpStatusCode.OK]: {
41+
data: allForks.ExecutionPayload | allForks.ExecutionPayloadAndBlobsBundle;
42+
version: ForkName;
43+
};
44+
},
4245
HttpStatusCode.SERVICE_UNAVAILABLE
4346
>
4447
>;
@@ -84,8 +87,13 @@ export function getReturnTypes(): ReturnTypes<Api> {
8487
getHeader: WithVersion((fork: ForkName) =>
8588
isForkExecution(fork) ? ssz.allForksExecution[fork].SignedBuilderBid : ssz.bellatrix.SignedBuilderBid
8689
),
87-
submitBlindedBlock: WithVersion((fork: ForkName) =>
88-
isForkExecution(fork) ? ssz.allForksExecution[fork].ExecutionPayload : ssz.bellatrix.ExecutionPayload
90+
submitBlindedBlock: WithVersion<allForks.ExecutionPayload | allForks.ExecutionPayloadAndBlobsBundle>(
91+
(fork: ForkName) =>
92+
isForkBlobs(fork)
93+
? ssz.allForksBlobs[fork].ExecutionPayloadAndBlobsBundle
94+
: isForkExecution(fork)
95+
? ssz.allForksExecution[fork].ExecutionPayload
96+
: ssz.bellatrix.ExecutionPayload
8997
),
9098
};
9199
}

packages/beacon-node/src/api/impl/beacon/blocks/index.ts

+21-79
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import {fromHexString, toHexString} from "@chainsafe/ssz";
22
import {routes, ServerApi, ResponseFormat} from "@lodestar/api";
3-
import {computeTimeAtSlot, signedBlindedBlockToFull, signedBlindedBlobSidecarsToFull} from "@lodestar/state-transition";
3+
import {
4+
computeTimeAtSlot,
5+
parseSignedBlindedBlockOrContents,
6+
reconstructFullBlockOrContents,
7+
} from "@lodestar/state-transition";
48
import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
5-
import {sleep, toHex, LogDataBasic} from "@lodestar/utils";
6-
import {allForks, deneb, isSignedBlockContents, isSignedBlindedBlockContents} from "@lodestar/types";
9+
import {sleep, toHex} from "@lodestar/utils";
10+
import {allForks, deneb, isSignedBlockContents, ProducedBlockSource} from "@lodestar/types";
711
import {BlockSource, getBlockInput, ImportBlockOpts, BlockInput} from "../../../../chain/blocks/types.js";
812
import {promiseAllMaybeAsync} from "../../../../util/promises.js";
913
import {isOptimisticBlock} from "../../../../util/forkChoice.js";
@@ -15,11 +19,6 @@ import {resolveBlockId, toBeaconHeaderResponse} from "./utils.js";
1519

1620
type PublishBlockOpts = ImportBlockOpts & {broadcastValidation?: routes.beacon.BroadcastValidation};
1721

18-
type ParsedSignedBlindedBlockOrContents = {
19-
signedBlindedBlock: allForks.SignedBlindedBeaconBlock;
20-
signedBlindedBlobSidecars: deneb.SignedBlindedBlobSidecars | null;
21-
};
22-
2322
/**
2423
* Validator clock may be advanced from beacon's clock. If the validator requests a resource in a
2524
* future slot, wait some time instead of rejecting the request because it's in the future
@@ -152,27 +151,28 @@ export function getBeaconBlockApi({
152151
.getBlindedForkTypes(signedBlindedBlock.message.slot)
153152
.BeaconBlock.hashTreeRoot(signedBlindedBlock.message)
154153
);
155-
const logCtx = {blockRoot, slot};
156154

157155
// Either the payload/blobs are cached from i) engine locally or ii) they are from the builder
158156
//
159-
// executionPayload can be null or a real payload in locally produced, its only undefined when
160-
// the block came from the builder
161-
const executionPayload = chain.producedBlockRoot.get(blockRoot);
157+
// executionPayload can be null or a real payload in locally produced so check for presence of root
158+
const source = chain.producedBlockRoot.has(blockRoot) ? ProducedBlockSource.engine : ProducedBlockSource.builder;
159+
160+
const executionPayload = chain.producedBlockRoot.get(blockRoot) ?? null;
161+
const blobSidecars = executionPayload
162+
? chain.producedBlobSidecarsCache.get(toHex(executionPayload.blockHash))
163+
: undefined;
164+
const blobs = blobSidecars ? blobSidecars.map((blobSidecar) => blobSidecar.blob) : null;
165+
162166
const signedBlockOrContents =
163-
executionPayload !== undefined
164-
? reconstructLocalBlockOrContents(
165-
chain,
166-
{signedBlindedBlock, signedBlindedBlobSidecars},
167-
executionPayload,
168-
logCtx
169-
)
170-
: await reconstructBuilderBlockOrContents(chain, signedBlindedBlockOrContents, logCtx);
167+
source === ProducedBlockSource.engine
168+
? reconstructFullBlockOrContents({signedBlindedBlock, signedBlindedBlobSidecars}, {executionPayload, blobs})
169+
: await reconstructBuilderBlockOrContents(chain, signedBlindedBlockOrContents);
171170

172171
// the full block is published by relay and it's possible that the block is already known to us
173172
// by gossip
174173
//
175174
// see: https://github.com/ChainSafe/lodestar/issues/5404
175+
chain.logger.info("Publishing assembled block", {blockRoot, slot, source});
176176
return publishBlock(signedBlockOrContents, {...opts, ignoreIfKnown: true});
177177
};
178178

@@ -365,73 +365,15 @@ export function getBeaconBlockApi({
365365
};
366366
}
367367

368-
function parseSignedBlindedBlockOrContents(
369-
signedBlindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents
370-
): ParsedSignedBlindedBlockOrContents {
371-
if (isSignedBlindedBlockContents(signedBlindedBlockOrContents)) {
372-
const signedBlindedBlock = signedBlindedBlockOrContents.signedBlindedBlock;
373-
const signedBlindedBlobSidecars = signedBlindedBlockOrContents.signedBlindedBlobSidecars;
374-
return {signedBlindedBlock, signedBlindedBlobSidecars};
375-
} else {
376-
return {signedBlindedBlock: signedBlindedBlockOrContents, signedBlindedBlobSidecars: null};
377-
}
378-
}
379-
380-
function reconstructLocalBlockOrContents(
381-
chain: ApiModules["chain"],
382-
{signedBlindedBlock, signedBlindedBlobSidecars}: ParsedSignedBlindedBlockOrContents,
383-
executionPayload: allForks.ExecutionPayload | null,
384-
logCtx: Record<string, LogDataBasic>
385-
): allForks.SignedBeaconBlockOrContents {
386-
const signedBlock = signedBlindedBlockToFull(signedBlindedBlock, executionPayload);
387-
if (executionPayload !== null) {
388-
Object.assign(logCtx, {transactions: executionPayload.transactions.length});
389-
}
390-
391-
if (signedBlindedBlobSidecars !== null) {
392-
if (executionPayload === null) {
393-
throw Error("Missing locally produced executionPayload for deneb+ publishBlindedBlock");
394-
}
395-
396-
const blockHash = toHex(executionPayload.blockHash);
397-
const blobSidecars = chain.producedBlobSidecarsCache.get(blockHash);
398-
if (blobSidecars === undefined) {
399-
throw Error("Missing blobSidecars from the local execution cache");
400-
}
401-
if (blobSidecars.length !== signedBlindedBlobSidecars.length) {
402-
throw Error(
403-
`Length mismatch signedBlindedBlobSidecars=${signedBlindedBlobSidecars.length} blobSidecars=${blobSidecars.length}`
404-
);
405-
}
406-
const signedBlobSidecars = signedBlindedBlobSidecarsToFull(
407-
signedBlindedBlobSidecars,
408-
blobSidecars.map((blobSidecar) => blobSidecar.blob)
409-
);
410-
411-
Object.assign(logCtx, {blobs: signedBlindedBlobSidecars.length});
412-
chain.logger.verbose("Block & blobs assembled from locally cached payload", logCtx);
413-
return {signedBlock, signedBlobSidecars} as allForks.SignedBeaconBlockOrContents;
414-
} else {
415-
chain.logger.verbose("Block assembled from locally cached payload", logCtx);
416-
return signedBlock as allForks.SignedBeaconBlockOrContents;
417-
}
418-
}
419-
420368
async function reconstructBuilderBlockOrContents(
421369
chain: ApiModules["chain"],
422-
signedBlindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents,
423-
logCtx: Record<string, LogDataBasic>
370+
signedBlindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents
424371
): Promise<allForks.SignedBeaconBlockOrContents> {
425-
// Mechanism for blobs & blocks on builder is implemenented separately in a followup deneb-builder PR
426-
if (isSignedBlindedBlockContents(signedBlindedBlockOrContents)) {
427-
throw Error("exeutionBuilder not yet implemented for deneb+ forks");
428-
}
429372
const executionBuilder = chain.executionBuilder;
430373
if (!executionBuilder) {
431374
throw Error("exeutionBuilder required to publish SignedBlindedBeaconBlock");
432375
}
433376

434377
const signedBlockOrContents = await executionBuilder.submitBlindedBlock(signedBlindedBlockOrContents);
435-
chain.logger.verbose("Publishing block assembled from the builder", logCtx);
436378
return signedBlockOrContents;
437379
}

packages/beacon-node/src/api/impl/validator/index.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -310,14 +310,17 @@ export function getValidatorApi({
310310

311311
const version = config.getForkName(block.slot);
312312
if (isForkBlobs(version)) {
313-
if (!isBlindedBlockContents(block)) {
314-
throw Error(`Expected BlockContents response at fork=${version}`);
313+
const blockHash = toHex((block as bellatrix.BlindedBeaconBlock).body.executionPayloadHeader.blockHash);
314+
const blindedBlobSidecars = chain.producedBlindedBlobSidecarsCache.get(blockHash);
315+
if (blindedBlobSidecars === undefined) {
316+
throw Error("blobSidecars missing in cache");
315317
}
316-
return {data: block, version, executionPayloadValue};
318+
return {
319+
data: {blindedBlock: block, blindedBlobSidecars} as allForks.BlindedBlockContents,
320+
version,
321+
executionPayloadValue,
322+
};
317323
} else {
318-
if (isBlindedBlockContents(block)) {
319-
throw Error(`Invalid BlockContents response at fork=${version}`);
320-
}
321324
return {data: block, version, executionPayloadValue};
322325
}
323326
} finally {

packages/beacon-node/src/chain/chain.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export class BeaconChain implements IBeaconChain {
131131
readonly checkpointBalancesCache: CheckpointBalancesCache;
132132
/** Map keyed by executionPayload.blockHash of the block for those blobs */
133133
readonly producedBlobSidecarsCache = new Map<BlockHash, deneb.BlobSidecars>();
134+
readonly producedBlindedBlobSidecarsCache = new Map<BlockHash, deneb.BlindedBlobSidecars>();
134135

135136
// Cache payload from the local execution so that produceBlindedBlock or produceBlockV3 and
136137
// send and get signed/published blinded versions which beacon can assemble into full before
@@ -522,7 +523,7 @@ export class BeaconChain implements IBeaconChain {
522523
// publishing the blinded block's full version
523524
if (blobs.type === BlobsResultType.produced) {
524525
// body is of full type here
525-
const blockHash = toHex((block as bellatrix.BeaconBlock).body.executionPayload.blockHash);
526+
const blockHash = blobs.blockHash;
526527
const blobSidecars = blobs.blobSidecars.map((blobSidecar) => ({
527528
...blobSidecar,
528529
blockRoot,
@@ -533,6 +534,21 @@ export class BeaconChain implements IBeaconChain {
533534

534535
this.producedBlobSidecarsCache.set(blockHash, blobSidecars);
535536
this.metrics?.blockProductionCaches.producedBlobSidecarsCache.set(this.producedBlobSidecarsCache.size);
537+
} else if (blobs.type === BlobsResultType.blinded) {
538+
// body is of blinded type here
539+
const blockHash = blobs.blockHash;
540+
const blindedBlobSidecars = blobs.blobSidecars.map((blindedBlobSidecar) => ({
541+
...blindedBlobSidecar,
542+
blockRoot,
543+
slot,
544+
blockParentRoot: parentBlockRoot,
545+
proposerIndex,
546+
}));
547+
548+
this.producedBlindedBlobSidecarsCache.set(blockHash, blindedBlobSidecars);
549+
this.metrics?.blockProductionCaches.producedBlindedBlobSidecarsCache.set(
550+
this.producedBlindedBlobSidecarsCache.size
551+
);
536552
}
537553

538554
return {block, executionPayloadValue};
@@ -792,6 +808,14 @@ export class BeaconChain implements IBeaconChain {
792808
this.opts.maxCachedBlobSidecars ?? DEFAULT_MAX_CACHED_BLOB_SIDECARS
793809
);
794810
this.metrics?.blockProductionCaches.producedBlobSidecarsCache.set(this.producedBlobSidecarsCache.size);
811+
812+
pruneSetToMax(
813+
this.producedBlindedBlobSidecarsCache,
814+
this.opts.maxCachedBlobSidecars ?? DEFAULT_MAX_CACHED_BLOB_SIDECARS
815+
);
816+
this.metrics?.blockProductionCaches.producedBlindedBlobSidecarsCache.set(
817+
this.producedBlindedBlobSidecarsCache.size
818+
);
795819
}
796820

797821
const metrics = this.metrics;

packages/beacon-node/src/chain/interface.ts

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export interface IBeaconChain {
9595
readonly checkpointBalancesCache: CheckpointBalancesCache;
9696
readonly producedBlobSidecarsCache: Map<BlockHash, deneb.BlobSidecars>;
9797
readonly producedBlockRoot: Map<RootHex, allForks.ExecutionPayload | null>;
98+
readonly producedBlindedBlobSidecarsCache: Map<BlockHash, deneb.BlindedBlobSidecars>;
9899
readonly producedBlindedBlockRoot: Set<RootHex>;
99100
readonly opts: IChainOptions;
100101

packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts

+49-15
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ import {PayloadId, IExecutionEngine, IExecutionBuilder, PayloadAttributes} from
3535
import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js";
3636
import {IEth1ForBlockProduction} from "../../eth1/index.js";
3737
import {numToQuantity} from "../../eth1/provider/utils.js";
38-
import {validateBlobsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js";
38+
import {
39+
validateBlobsAndKzgCommitments,
40+
validateBlindedBlobsAndKzgCommitments,
41+
} from "./validateBlobsAndKzgCommitments.js";
3942

4043
// Time to provide the EL to generate a payload from new payload id
4144
const PAYLOAD_GENERATION_TIME_MS = 500;
@@ -70,8 +73,9 @@ export enum BlobsResultType {
7073
}
7174

7275
export type BlobsResult =
73-
| {type: BlobsResultType.preDeneb | BlobsResultType.blinded}
74-
| {type: BlobsResultType.produced; blobSidecars: deneb.BlobSidecars; blockHash: RootHex};
76+
| {type: BlobsResultType.preDeneb}
77+
| {type: BlobsResultType.produced; blobSidecars: deneb.BlobSidecars; blockHash: RootHex}
78+
| {type: BlobsResultType.blinded; blobSidecars: deneb.BlindedBlobSidecars; blockHash: RootHex};
7579

7680
export async function produceBlockBody<T extends BlockType>(
7781
this: BeaconChain,
@@ -195,16 +199,47 @@ export async function produceBlockBody<T extends BlockType>(
195199
);
196200
(blockBody as allForks.BlindedBeaconBlockBody).executionPayloadHeader = builderRes.header;
197201
executionPayloadValue = builderRes.executionPayloadValue;
198-
this.logger.verbose("Fetched execution payload header from builder", {slot: blockSlot, executionPayloadValue});
202+
203+
const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
204+
const prepType = "blinded";
205+
this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
206+
this.logger.verbose("Fetched execution payload header from builder", {
207+
slot: blockSlot,
208+
executionPayloadValue,
209+
prepType,
210+
fetchedTime,
211+
});
212+
199213
if (ForkSeq[fork] >= ForkSeq.deneb) {
200-
const {blobKzgCommitments} = builderRes;
201-
if (blobKzgCommitments === undefined) {
202-
throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
214+
const {blindedBlobsBundle} = builderRes;
215+
if (blindedBlobsBundle === undefined) {
216+
throw Error(`Invalid builder getHeader response for fork=${fork}, missing blindedBlobsBundle`);
203217
}
204-
(blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments;
205-
blobsResult = {type: BlobsResultType.blinded};
206218

207-
Object.assign(logMeta, {blobs: blobKzgCommitments.length});
219+
// validate blindedBlobsBundle
220+
if (this.opts.sanityCheckExecutionEngineBlobs) {
221+
validateBlindedBlobsAndKzgCommitments(builderRes.header, blindedBlobsBundle);
222+
}
223+
224+
(blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blindedBlobsBundle.commitments;
225+
const blockHash = toHex(builderRes.header.blockHash);
226+
227+
const blobSidecars = Array.from({length: blindedBlobsBundle.blobRoots.length}, (_v, index) => {
228+
const blobRoot = blindedBlobsBundle.blobRoots[index];
229+
const commitment = blindedBlobsBundle.commitments[index];
230+
const proof = blindedBlobsBundle.proofs[index];
231+
const blindedBlobSidecar = {
232+
index,
233+
blobRoot,
234+
kzgProof: proof,
235+
kzgCommitment: commitment,
236+
};
237+
// Other fields will be injected after postState is calculated
238+
return blindedBlobSidecar;
239+
}) as deneb.BlindedBlobSidecars;
240+
blobsResult = {type: BlobsResultType.blinded, blobSidecars, blockHash};
241+
242+
Object.assign(logMeta, {blobs: blindedBlobsBundle.commitments.length});
208243
} else {
209244
blobsResult = {type: BlobsResultType.preDeneb};
210245
}
@@ -270,7 +305,7 @@ export async function produceBlockBody<T extends BlockType>(
270305
throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
271306
}
272307

273-
// Optionally sanity-check that the KZG commitments match the versioned hashes in the transactions
308+
// validate blindedBlobsBundle
274309
if (this.opts.sanityCheckExecutionEngineBlobs) {
275310
validateBlobsAndKzgCommitments(executionPayload, blobsBundle);
276311
}
@@ -288,6 +323,7 @@ export async function produceBlockBody<T extends BlockType>(
288323
kzgProof: proof,
289324
kzgCommitment: commitment,
290325
};
326+
// Other fields will be injected after postState is calculated
291327
return blobSidecar;
292328
}) as deneb.BlobSidecars;
293329
blobsResult = {type: BlobsResultType.produced, blobSidecars, blockHash};
@@ -443,21 +479,19 @@ async function prepareExecutionPayloadHeader(
443479
): Promise<{
444480
header: allForks.ExecutionPayloadHeader;
445481
executionPayloadValue: Wei;
446-
blobKzgCommitments?: deneb.BlobKzgCommitments;
482+
blindedBlobsBundle?: deneb.BlindedBlobsBundle;
447483
}> {
448484
if (!chain.executionBuilder) {
449485
throw Error("executionBuilder required");
450486
}
451487

452488
const parentHashRes = await getExecutionPayloadParentHash(chain, state);
453-
454489
if (parentHashRes.isPremerge) {
455-
// TODO: Is this okay?
456490
throw Error("Execution builder disabled pre-merge");
457491
}
458492

459493
const {parentHash} = parentHashRes;
460-
return chain.executionBuilder.getHeader(state.slot, parentHash, proposerPubKey);
494+
return chain.executionBuilder.getHeader(fork, state.slot, parentHash, proposerPubKey);
461495
}
462496

463497
export async function getExecutionPayloadParentHash(

0 commit comments

Comments
 (0)