Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions packages/api/src/builder/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ssz, allForks, bellatrix, Slot, Root, BLSPubkey} from "@lodestar/types";
import {fromHexString, toHexString} from "@chainsafe/ssz";
import {ForkName} from "@lodestar/params";
import {ForkName, isForkExecution, isForkBlobs} from "@lodestar/params";
import {IChainForkConfig} from "@lodestar/config";

import {
Expand All @@ -24,10 +24,13 @@ export type Api = {
slot: Slot,
parentHash: Root,
proposerPubKey: BLSPubkey
): Promise<{version: ForkName; data: bellatrix.SignedBuilderBid}>;
): Promise<{version: ForkName; data: allForks.SignedBuilderBid}>;
submitBlindedBlock(
signedBlock: allForks.SignedBlindedBeaconBlock
): Promise<{version: ForkName; data: allForks.ExecutionPayload}>;
submitBlindedBlockV2(
signedBlock: allForks.SignedBlindedBeaconBlock
): Promise<{version: ForkName; data: allForks.SignedBeaconBlockAndBlobsSidecar}>;
};

/**
Expand All @@ -38,6 +41,7 @@ export const routesData: RoutesData<Api> = {
registerValidator: {url: "/eth/v1/builder/validators", method: "POST"},
getHeader: {url: "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}", method: "GET"},
submitBlindedBlock: {url: "/eth/v1/builder/blinded_blocks", method: "POST"},
submitBlindedBlockV2: {url: "/eth/v2/builder/blinded_blocks", method: "POST"},
};

/* eslint-disable @typescript-eslint/naming-convention */
Expand All @@ -46,6 +50,7 @@ export type ReqTypes = {
registerValidator: {body: unknown};
getHeader: {params: {slot: Slot; parent_hash: string; pubkey: string}};
submitBlindedBlock: {body: unknown};
submitBlindedBlockV2: {body: unknown};
};

export function getReqSerializers(config: IChainForkConfig): ReqSerializers<Api, ReqTypes> {
Expand All @@ -62,13 +67,22 @@ export function getReqSerializers(config: IChainForkConfig): ReqSerializers<Api,
},
},
submitBlindedBlock: getBeaconReqSerializers(config)["publishBlindedBlock"],
submitBlindedBlockV2: getBeaconReqSerializers(config)["publishBlindedBlock"],
};
}

export function getReturnTypes(): ReturnTypes<Api> {
return {
// TODO: Generalize to allForks
getHeader: WithVersion(() => ssz.bellatrix.SignedBuilderBid),
submitBlindedBlock: WithVersion(() => ssz.bellatrix.ExecutionPayload),
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
),
submitBlindedBlockV2: WithVersion((fork: ForkName) =>
isForkBlobs(fork)
? ssz.allForksBlobs[fork].SignedBeaconBlockAndBlobsSidecar
: ssz.eip4844.SignedBeaconBlockAndBlobsSidecar
),
};
}
9 changes: 7 additions & 2 deletions packages/api/test/unit/builder/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import {testData} from "./testData.js";

describe("builder", () => {
runGenericServerTest<Api, ReqTypes>(
// eslint-disable-next-line @typescript-eslint/naming-convention
createIChainForkConfig({...defaultChainConfig, ALTAIR_FORK_EPOCH: 0, BELLATRIX_FORK_EPOCH: 0}),
createIChainForkConfig({
...defaultChainConfig,
/* eslint-disable @typescript-eslint/naming-convention */
ALTAIR_FORK_EPOCH: 0,
BELLATRIX_FORK_EPOCH: 0,
EIP4844_FORK_EPOCH: 0,
}),
getClient,
getRoutes,
testData
Expand Down
6 changes: 5 additions & 1 deletion packages/api/test/unit/builder/testData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ export const testData: GenericServerTestCases<Api> = {
res: {version: ForkName.bellatrix, data: ssz.bellatrix.SignedBuilderBid.defaultValue()},
},
submitBlindedBlock: {
args: [ssz.bellatrix.SignedBlindedBeaconBlock.defaultValue()],
args: [ssz.eip4844.SignedBlindedBeaconBlock.defaultValue()],
res: {version: ForkName.bellatrix, data: ssz.bellatrix.ExecutionPayload.defaultValue()},
},
submitBlindedBlockV2: {
args: [ssz.eip4844.SignedBlindedBeaconBlock.defaultValue()],
res: {version: ForkName.eip4844, data: ssz.eip4844.SignedBeaconBlockAndBlobsSidecar.defaultValue()},
},
};
18 changes: 16 additions & 2 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {routes} from "@lodestar/api";
import {computeTimeAtSlot} from "@lodestar/state-transition";
import {ForkSeq, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {sleep} from "@lodestar/utils";
import {eip4844} from "@lodestar/types";
import {eip4844, allForks} from "@lodestar/types";
import {fromHexString, toHexString} from "@chainsafe/ssz";
import {getBlockInput} from "../../../../chain/blocks/types.js";
import {promiseAllMaybeAsync} from "../../../../util/promises.js";
Expand Down Expand Up @@ -181,7 +181,21 @@ export function getBeaconBlockApi({
async publishBlindedBlock(signedBlindedBlock) {
const executionBuilder = chain.executionBuilder;
if (!executionBuilder) throw Error("exeutionBuilder required to publish SignedBlindedBeaconBlock");
const signedBlock = await executionBuilder.submitBlindedBlock(signedBlindedBlock);
let signedBlock: allForks.SignedBeaconBlock;
if (config.getForkSeq(signedBlindedBlock.message.slot) >= ForkSeq.eip4844) {
const {beaconBlock, blobsSidecar} = await executionBuilder.submitBlindedBlockV2(signedBlindedBlock);
signedBlock = beaconBlock;
// add this blobs to the map for access & broadcasting in publishBlock
const {blockHash} = signedBlindedBlock.message.body.executionPayloadHeader;
chain.producedBlobsSidecarCache.set(toHexString(blockHash), blobsSidecar);
// TODO: Do we need to prune here ? prune will anyway be called in local execution flow
// pruneSetToMax(
// chain.producedBlobsSidecarCache,
// chain.opts.maxCachedBlobsSidecar ?? DEFAULT_MAX_CACHED_BLOBS_SIDECAR
// );
} else {
signedBlock = await executionBuilder.submitBlindedBlock(signedBlindedBlock);
}
return await this.publishBlock(signedBlock);
},

Expand Down
10 changes: 6 additions & 4 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ export class BeaconChain implements IBeaconChain {

readonly beaconProposerCache: BeaconProposerCache;
readonly checkpointBalancesCache: CheckpointBalancesCache;
// TODO EIP-4844: Prune data structure every time period, for both old entries
/** Map keyed by executionPayload.blockHash of the block for those blobs */
readonly producedBlobsSidecarCache = new Map<RootHex, eip4844.BlobsSidecar>();
readonly opts: IChainOptions;

protected readonly blockProcessor: BlockProcessor;
Expand All @@ -127,10 +130,6 @@ export class BeaconChain implements IBeaconChain {
private successfulExchangeTransition = false;
private readonly exchangeTransitionConfigurationEverySlots: number;

// TODO EIP-4844: Prune data structure every time period, for both old entries
/** Map keyed by executionPayload.blockHash of the block for those blobs */
private readonly producedBlobsSidecarCache = new Map<RootHex, eip4844.BlobsSidecar>();

private readonly faultInspectionWindow: number;
private readonly allowedFaults: number;
private processShutdownCallback: ProcessShutdownCallback;
Expand Down Expand Up @@ -393,6 +392,9 @@ export class BeaconChain implements IBeaconChain {
block.stateRoot = computeNewStateRoot(this.metrics, state, block);

// Cache for latter broadcasting
//
// blinded blobs will be fetched and added to this cache later before finally
// publishing the blinded block's full version
if (blobs.type === BlobsResultType.produced) {
// TODO EIP-4844: Prune data structure for max entries
this.producedBlobsSidecarCache.set(blobs.blockHash, {
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 @@ -88,6 +88,7 @@ export interface IBeaconChain {

readonly beaconProposerCache: BeaconProposerCache;
readonly checkpointBalancesCache: CheckpointBalancesCache;
readonly producedBlobsSidecarCache: Map<RootHex, eip4844.BlobsSidecar>;
readonly opts: IChainOptions;

/** Stop beacon chain processing */
Expand Down
53 changes: 27 additions & 26 deletions packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
getExpectedWithdrawals,
} from "@lodestar/state-transition";
import {IChainForkConfig} from "@lodestar/config";
import {ForkName, ForkSeq, ForkExecution} from "@lodestar/params";
import {ForkSeq, ForkExecution, isForkExecution} from "@lodestar/params";
import {toHex, sleep} from "@lodestar/utils";

import type {BeaconChain} from "../chain.js";
Expand Down Expand Up @@ -64,10 +64,11 @@ export type AssembledBlockType<T extends BlockType> = T extends BlockType.Full
export enum BlobsResultType {
preEIP4844,
produced,
blinded,
}

export type BlobsResult =
| {type: BlobsResultType.preEIP4844}
| {type: BlobsResultType.preEIP4844 | BlobsResultType.blinded}
| {type: BlobsResultType.produced; blobs: eip4844.Blobs; blockHash: RootHex};

export async function produceBlockBody<T extends BlockType>(
Expand All @@ -89,8 +90,10 @@ export async function produceBlockBody<T extends BlockType>(
proposerPubKey: BLSPubkey;
}
): Promise<{body: AssembledBodyType<T>; blobs: BlobsResult}> {
// We assign this in an EIP-4844 branch below and return it
let blobs: {blobs: eip4844.Blobs; blockHash: RootHex} | null = null;
// Type-safe for blobs variable. Translate 'null' value into 'preEIP4844' enum
// TODO: Not ideal, but better than just using null.
// TODO: Does not guarantee that preEIP4844 enum goes with a preEIP4844 block
let blobsResult: BlobsResult;

// TODO:
// Iterate through the naive aggregation pool and ensure all the attestations from there
Expand Down Expand Up @@ -135,7 +138,7 @@ export async function produceBlockBody<T extends BlockType>(

const fork = currentState.config.getForkName(blockSlot);

if (fork !== ForkName.phase0 && fork !== ForkName.altair) {
if (isForkExecution(fork)) {
const safeBlockHash = this.forkChoice.getJustifiedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
const feeRecipient = this.beaconProposerCache.getOrDefault(proposerIndex);
Expand All @@ -160,17 +163,22 @@ export async function produceBlockBody<T extends BlockType>(
// For MeV boost integration, this is where the execution header will be
// fetched from the payload id and a blinded block will be produced instead of
// fullblock for the validator to sign
(blockBody as allForks.BlindedBeaconBlockBody).executionPayloadHeader = await prepareExecutionPayloadHeader(
const builderRes = await prepareExecutionPayloadHeader(
this,
fork,
currentState as CachedBeaconStateBellatrix,
proposerPubKey
);

// Capella and later forks have withdrawalRoot on their ExecutionPayloadHeader
// TODO Capella: Remove this. It will come from the execution client.
if (ForkSeq[fork] >= ForkSeq.capella) {
throw Error("Builder blinded blocks not supported after capella");
(blockBody as allForks.BlindedBeaconBlockBody).executionPayloadHeader = builderRes.header;
if (ForkSeq[fork] >= ForkSeq.eip4844) {
const {blobKzgCommitments} = builderRes as {blobKzgCommitments: eip4844.BlobKzgCommitments};
if (blobKzgCommitments === undefined) {
throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
}
(blockBody as eip4844.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments;
blobsResult = {type: BlobsResultType.blinded};
} else {
blobsResult = {type: BlobsResultType.preEIP4844};
}
}

Expand All @@ -194,6 +202,7 @@ export async function produceBlockBody<T extends BlockType>(
(blockBody as allForks.ExecutionBlockBody).executionPayload = ssz.allForksExecution[
fork
].ExecutionPayload.defaultValue();
blobsResult = {type: BlobsResultType.preEIP4844};
} else {
const {prepType, payloadId} = prepareRes;
if (prepType !== PayloadPreparationType.Cached) {
Expand Down Expand Up @@ -233,7 +242,9 @@ export async function produceBlockBody<T extends BlockType>(
}

(blockBody as eip4844.BeaconBlockBody).blobKzgCommitments = blobsBundle.kzgs;
blobs = {blobs: blobsBundle.blobs, blockHash};
blobsResult = {type: BlobsResultType.produced, blobs: blobsBundle.blobs, blockHash};
} else {
blobsResult = {type: BlobsResultType.preEIP4844};
}
}
} catch (e) {
Expand All @@ -250,33 +261,23 @@ export async function produceBlockBody<T extends BlockType>(
(blockBody as allForks.ExecutionBlockBody).executionPayload = ssz.allForksExecution[
fork
].ExecutionPayload.defaultValue();
blobsResult = {type: BlobsResultType.preEIP4844};
} else {
// since merge transition is complete, we need a valid payload even if with an
// empty (transactions) one. defaultValue isn't gonna cut it!
throw e;
}
}
}
} else {
blobsResult = {type: BlobsResultType.preEIP4844};
}

if (ForkSeq[fork] >= ForkSeq.capella) {
// TODO: blsToExecutionChanges should be passed in the produceBlock call
(blockBody as capella.BeaconBlockBody).blsToExecutionChanges = blsToExecutionChanges;
}

// Type-safe for blobs variable. Translate 'null' value into 'preEIP4844' enum
// TODO: Not ideal, but better than just using null.
// TODO: Does not guarantee that preEIP4844 enum goes with a preEIP4844 block
let blobsResult: BlobsResult;
if (ForkSeq[fork] >= ForkSeq.eip4844) {
if (!blobs) {
throw Error("Blobs are null post eip4844");
}
blobsResult = {type: BlobsResultType.produced, ...blobs};
} else {
blobsResult = {type: BlobsResultType.preEIP4844};
}

return {body: blockBody as AssembledBodyType<T>, blobs: blobsResult};
}

Expand Down Expand Up @@ -375,7 +376,7 @@ async function prepareExecutionPayloadHeader(
fork: ForkExecution,
state: CachedBeaconStateBellatrix,
proposerPubKey: BLSPubkey
): Promise<allForks.ExecutionPayloadHeader> {
): Promise<{header: allForks.ExecutionPayloadHeader; blobKzgCommitments?: eip4844.BlobKzgCommitments}> {
if (!chain.executionBuilder) {
throw Error("executionBuilder required");
}
Expand Down
Loading