Skip to content

Commit

Permalink
feat: enable builder proposals post deneb with blobs
Browse files Browse the repository at this point in the history
implement missing blindedblock publishing
  • Loading branch information
g11tech committed Oct 10, 2023
1 parent 9ea7037 commit 8bf1b57
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 49 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
),
};
}
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
20 changes: 18 additions & 2 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class BeaconChain implements IBeaconChain {
readonly producedBlobSidecarsCache = new Map<BlockHash, {blobSidecars: deneb.BlobSidecars; slot: Slot}>();
readonly producedBlindedBlobSidecarsCache = new Map<
BlockHash,
{blobSidecars: deneb.BlindedBlobSidecars; slot: Slot}
{blindedBlobSidecars: deneb.BlindedBlobSidecars; slot: Slot}
>();

// Cache payload from the local execution so that produceBlindedBlock or produceBlockV3 and
Expand Down Expand Up @@ -531,7 +531,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 @@ -545,6 +545,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};
Expand Down
5 changes: 4 additions & 1 deletion packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ export interface IBeaconChain {
readonly beaconProposerCache: BeaconProposerCache;
readonly checkpointBalancesCache: CheckpointBalancesCache;
readonly producedBlobSidecarsCache: Map<BlockHash, {blobSidecars: deneb.BlobSidecars; slot: Slot}>;
readonly producedBlindedBlobSidecarsCache: Map<BlockHash, {blobSidecars: deneb.BlindedBlobSidecars; slot: Slot}>;
readonly producedBlockRoot: Map<RootHex, allForks.ExecutionPayload | null>;
readonly producedBlindedBlobSidecarsCache: Map<
BlockHash,
{blindedBlobSidecars: deneb.BlindedBlobSidecars; slot: Slot}
>;
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {allForks} from "@lodestar/types";
import {allForks, deneb} from "@lodestar/types";
import {BlobsBundle} from "../../execution/index.js";

/**
Expand All @@ -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}`
);
}
}
93 changes: 79 additions & 14 deletions packages/beacon-node/src/execution/builder/http.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
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";
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";
Expand Down Expand Up @@ -91,38 +102,92 @@ 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<allForks.SignedBeaconBlock> {
const res = await this.api.submitBlindedBlock(signedBlock);
async submitBlindedBlock(
signedBlindedBlockOrContents: allForks.SignedBlindedBeaconBlockOrContents
): Promise<allForks.SignedBeaconBlockOrContents> {
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;

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;
}

// 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(
expectedTransactionsRoot
)}, actual=${toHexString(actualTransactionsRoot)}`
);
}
const fullySignedBlock: bellatrix.SignedBeaconBlock = {
...signedBlock,
message: {...signedBlock.message, body: {...signedBlock.message.body, executionPayload}},

const signedBlock: bellatrix.SignedBeaconBlock = {
...signedBlindedBlock,
message: {...signedBlindedBlock.message, body: {...signedBlindedBlock.message.body, executionPayload}},
};
return fullySignedBlock;

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;
}
}
}
Loading

0 comments on commit 8bf1b57

Please sign in to comment.