diff --git a/packages/api/src/builder/routes.ts b/packages/api/src/builder/routes.ts index 9afefa9e18ce..e3cf7214650c 100644 --- a/packages/api/src/builder/routes.ts +++ b/packages/api/src/builder/routes.ts @@ -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 { @@ -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}>; }; /** @@ -38,6 +41,7 @@ export const routesData: RoutesData = { 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 */ @@ -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 { @@ -62,13 +67,22 @@ export function getReqSerializers(config: IChainForkConfig): ReqSerializers { 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 + ), }; } diff --git a/packages/api/test/unit/builder/builder.test.ts b/packages/api/test/unit/builder/builder.test.ts index c5c4d90d41d7..8b9ac38333eb 100644 --- a/packages/api/test/unit/builder/builder.test.ts +++ b/packages/api/test/unit/builder/builder.test.ts @@ -7,8 +7,13 @@ import {testData} from "./testData.js"; describe("builder", () => { runGenericServerTest( - // 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 diff --git a/packages/api/test/unit/builder/testData.ts b/packages/api/test/unit/builder/testData.ts index 1ec67f5eee35..38a12ae6d637 100644 --- a/packages/api/test/unit/builder/testData.ts +++ b/packages/api/test/unit/builder/testData.ts @@ -23,7 +23,11 @@ export const testData: GenericServerTestCases = { 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()}, + }, }; 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 bb476049b8dd..1610a0bb5505 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -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"; @@ -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); }, diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index e435a6d81c8d..47d813bf0129 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -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(); readonly opts: IChainOptions; protected readonly blockProcessor: BlockProcessor; @@ -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(); - private readonly faultInspectionWindow: number; private readonly allowedFaults: number; private processShutdownCallback: ProcessShutdownCallback; @@ -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, { diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 50fe3033719f..945b884735ff 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -88,6 +88,7 @@ export interface IBeaconChain { readonly beaconProposerCache: BeaconProposerCache; readonly checkpointBalancesCache: CheckpointBalancesCache; + readonly producedBlobsSidecarCache: Map; readonly opts: IChainOptions; /** Stop beacon chain processing */ diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 7f2984bb0850..d828e41b924c 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -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"; @@ -64,10 +64,11 @@ export type AssembledBlockType = 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( @@ -89,8 +90,10 @@ export async function produceBlockBody( proposerPubKey: BLSPubkey; } ): Promise<{body: AssembledBodyType; 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 @@ -135,7 +138,7 @@ export async function produceBlockBody( 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); @@ -160,17 +163,22 @@ export async function produceBlockBody( // 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}; } } @@ -194,6 +202,7 @@ export async function produceBlockBody( (blockBody as allForks.ExecutionBlockBody).executionPayload = ssz.allForksExecution[ fork ].ExecutionPayload.defaultValue(); + blobsResult = {type: BlobsResultType.preEIP4844}; } else { const {prepType, payloadId} = prepareRes; if (prepType !== PayloadPreparationType.Cached) { @@ -233,7 +242,9 @@ export async function produceBlockBody( } (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) { @@ -250,6 +261,7 @@ export async function produceBlockBody( (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! @@ -257,6 +269,8 @@ export async function produceBlockBody( } } } + } else { + blobsResult = {type: BlobsResultType.preEIP4844}; } if (ForkSeq[fork] >= ForkSeq.capella) { @@ -264,19 +278,6 @@ export async function produceBlockBody( (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, blobs: blobsResult}; } @@ -375,7 +376,7 @@ async function prepareExecutionPayloadHeader( fork: ForkExecution, state: CachedBeaconStateBellatrix, proposerPubKey: BLSPubkey -): Promise { +): Promise<{header: allForks.ExecutionPayloadHeader; blobKzgCommitments?: eip4844.BlobKzgCommitments}> { if (!chain.executionBuilder) { throw Error("executionBuilder required"); } diff --git a/packages/beacon-node/src/execution/builder/http.ts b/packages/beacon-node/src/execution/builder/http.ts index 658d5b491bc6..5ce8411c08f8 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -1,8 +1,9 @@ -import {allForks, bellatrix, Slot, Root, BLSPubkey, ssz} from "@lodestar/types"; +import {allForks, bellatrix, Slot, Root, BLSPubkey, ssz, eip4844} from "@lodestar/types"; import {IChainForkConfig} from "@lodestar/config"; import {getClient, Api as BuilderApi} from "@lodestar/api/builder"; import {byteArrayEquals, toHexString} from "@chainsafe/ssz"; +import {validateBlobsAndKzgCommitments} from "../../chain/produceBlock/validateBlobsAndKzgCommitments.js"; import {IExecutionBuilder} from "./interface.js"; export type ExecutionBuilderHttpOpts = { @@ -21,6 +22,7 @@ export const defaultExecutionBuilderHttpOpts: ExecutionBuilderHttpOpts = { export class ExecutionBuilderHttp implements IExecutionBuilder { readonly api: BuilderApi; + readonly config: IChainForkConfig; readonly issueLocalFcUForBlockProduction?: boolean; // Builder needs to be explicity enabled using updateStatus status = false; @@ -29,6 +31,7 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { const baseUrl = opts.urls[0]; if (!baseUrl) throw Error("No Url provided for executionBuilder"); this.api = getClient({baseUrl, timeoutMs: opts.timeout}, {config}); + this.config = config; this.issueLocalFcUForBlockProduction = opts.issueLocalFcUForBlockProduction; } @@ -50,10 +53,13 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { return this.api.registerValidator(registrations); } - async getHeader(slot: Slot, parentHash: Root, proposerPubKey: BLSPubkey): Promise { + async getHeader( + slot: Slot, + parentHash: Root, + proposerPubKey: BLSPubkey + ): Promise<{header: allForks.ExecutionPayloadHeader; blobKzgCommitments?: eip4844.BlobKzgCommitments}> { const {data: signedBid} = await this.api.getHeader(slot, parentHash, proposerPubKey); - const executionPayloadHeader = signedBid.message.header; - return executionPayloadHeader; + return signedBid.message; } async submitBlindedBlock(signedBlock: allForks.SignedBlindedBeaconBlock): Promise { @@ -62,7 +68,7 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { const actualTransactionsRoot = ssz.bellatrix.Transactions.hashTreeRoot(executionPayload.transactions); if (!byteArrayEquals(expectedTransactionsRoot, actualTransactionsRoot)) { throw Error( - `Invald transactionsRoot of the builder payload, expected=${toHexString( + `Invalid transactionsRoot of the builder payload, expected=${toHexString( expectedTransactionsRoot )}, actual=${toHexString(actualTransactionsRoot)}` ); @@ -73,4 +79,46 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { }; return fullySignedBlock; } + + async submitBlindedBlockV2( + signedBlock: allForks.SignedBlindedBeaconBlock + ): Promise { + const {data: signedBeaconBlockAndBlobsSidecar} = await this.api.submitBlindedBlockV2(signedBlock); + // Since we get the full block back, we can just just compare the hash of blinded to returned + const {beaconBlock, blobsSidecar} = signedBeaconBlockAndBlobsSidecar; + + // Verify if the transactions and withdrawals match with their corresponding roots + // since we get the full signed block back, its easy to validate response consistency + // if the signed blinded and signed full root simply match + const signedBlockRoot = this.config + .getBlindedForkTypes(signedBlock.message.slot) + .SignedBeaconBlock.hashTreeRoot(signedBlock); + const beaconBlockRoot = this.config + .getForkTypes(beaconBlock.message.slot) + .SignedBeaconBlock.hashTreeRoot(beaconBlock); + if (!byteArrayEquals(signedBlockRoot, beaconBlockRoot)) { + throw Error( + `Invalid SignedBeaconBlock of the builder submitBlindedBlockV2 response, expected=${toHexString( + signedBlockRoot + )}, actual=${toHexString(beaconBlockRoot)}` + ); + } + + // Sanity check consistency between payload and blobs bundle still needs to be done + const payload = beaconBlock.message.body.executionPayload; + const blockHash = toHexString(payload.blockHash); + const blobsBlockHash = toHexString(blobsSidecar.beaconBlockRoot); + if (blockHash !== blobsBlockHash) { + throw Error(`blobsSidecar incorrect blockHash expected=${blockHash}, actual=${blobsBlockHash}`); + } + // Sanity-check that the KZG commitments match the versioned hashes in the transactions + const {blobKzgCommitments: kzgs} = beaconBlock.message.body as eip4844.BeaconBlockBody; + if (kzgs === undefined) { + throw Error("Missing blobKzgCommitments on beaconBlock's body"); + } + const {blobs} = blobsSidecar; + validateBlobsAndKzgCommitments(payload, {blockHash, kzgs, blobs}); + + return signedBeaconBlockAndBlobsSidecar; + } } diff --git a/packages/beacon-node/src/execution/builder/interface.ts b/packages/beacon-node/src/execution/builder/interface.ts index f08177436bfd..8be1a0853d10 100644 --- a/packages/beacon-node/src/execution/builder/interface.ts +++ b/packages/beacon-node/src/execution/builder/interface.ts @@ -1,4 +1,4 @@ -import {allForks, bellatrix, Root, Slot, BLSPubkey} from "@lodestar/types"; +import {allForks, bellatrix, Root, Slot, BLSPubkey, eip4844} from "@lodestar/types"; export interface IExecutionBuilder { /** @@ -11,6 +11,13 @@ export interface IExecutionBuilder { updateStatus(shouldEnable: boolean): void; checkStatus(): Promise; registerValidator(registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise; - getHeader(slot: Slot, parentHash: Root, proposerPubKey: BLSPubkey): Promise; + getHeader( + slot: Slot, + parentHash: Root, + proposerPubKey: BLSPubkey + ): Promise<{header: allForks.ExecutionPayloadHeader; blobKzgCommitments?: eip4844.BlobKzgCommitments}>; submitBlindedBlock(signedBlock: allForks.SignedBlindedBeaconBlock): Promise; + submitBlindedBlockV2( + signedBlock: allForks.SignedBlindedBeaconBlock + ): Promise; } diff --git a/packages/beacon-node/test/utils/mocks/chain/chain.ts b/packages/beacon-node/test/utils/mocks/chain/chain.ts index c6b31acaf578..5aae4e53c7d1 100644 --- a/packages/beacon-node/test/utils/mocks/chain/chain.ts +++ b/packages/beacon-node/test/utils/mocks/chain/chain.ts @@ -1,7 +1,7 @@ import sinon from "sinon"; import {CompositeTypeAny, toHexString, TreeView} from "@chainsafe/ssz"; -import {phase0, allForks, UintNum64, Root, Slot, ssz, Uint16, UintBn64} from "@lodestar/types"; +import {phase0, allForks, UintNum64, Root, Slot, ssz, Uint16, UintBn64, RootHex, eip4844} from "@lodestar/types"; import {IBeaconConfig} from "@lodestar/config"; import {BeaconStateAllForks, CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {CheckpointWithHex, IForkChoice, ProtoBlock, ExecutionStatus, AncestorStatus} from "@lodestar/fork-choice"; @@ -105,6 +105,8 @@ export class MockBeaconChain implements IBeaconChain { private readonly state: CachedBeaconStateAllForks; private abortController: AbortController; + readonly producedBlobsSidecarCache = new Map(); + constructor({genesisTime, chainId, networkId, state, config}: IMockChainParams) { this.logger = testLogger(); this.genesisTime = genesisTime ?? state.genesisTime; diff --git a/packages/config/src/forkConfig/index.ts b/packages/config/src/forkConfig/index.ts index 16337a286753..a75e45728fdf 100644 --- a/packages/config/src/forkConfig/index.ts +++ b/packages/config/src/forkConfig/index.ts @@ -1,4 +1,4 @@ -import {GENESIS_EPOCH, ForkName, SLOTS_PER_EPOCH, ForkSeq} from "@lodestar/params"; +import {GENESIS_EPOCH, ForkName, SLOTS_PER_EPOCH, ForkSeq, isForkExecution, isForkBlobs} from "@lodestar/params"; import {Slot, allForks, Version, ssz} from "@lodestar/types"; import {IChainConfig} from "../chainConfig/index.js"; import {IForkConfig, IForkInfo} from "./types.js"; @@ -84,17 +84,24 @@ export function createIForkConfig(config: IChainConfig): IForkConfig { }, getExecutionForkTypes(slot: Slot): allForks.AllForksExecutionSSZTypes { const forkName = this.getForkName(slot); - if (forkName === ForkName.phase0 || forkName === ForkName.altair) { - throw Error(`Invalid slot=${slot} fork=${forkName} for blinded fork types`); + if (!isForkExecution(forkName)) { + throw Error(`Invalid slot=${slot} fork=${forkName} for execution fork types`); } return ssz.allForksExecution[forkName] as allForks.AllForksExecutionSSZTypes; }, getBlindedForkTypes(slot: Slot): allForks.AllForksBlindedSSZTypes { const forkName = this.getForkName(slot); - if (forkName === ForkName.phase0 || forkName === ForkName.altair) { + if (!isForkExecution(forkName)) { throw Error(`Invalid slot=${slot} fork=${forkName} for blinded fork types`); } return ssz.allForksBlinded[forkName] as allForks.AllForksBlindedSSZTypes; }, + getBlobsForkTypes(slot: Slot): allForks.AllForksBlobsSSZTypes { + const forkName = this.getForkName(slot); + if (!isForkBlobs(forkName)) { + throw Error(`Invalid slot=${slot} fork=${forkName} for blobs fork types`); + } + return ssz.allForksBlobs[forkName] as allForks.AllForksBlobsSSZTypes; + }, }; } diff --git a/packages/config/src/forkConfig/types.ts b/packages/config/src/forkConfig/types.ts index d1d2a96fc86c..4be1ada8befb 100644 --- a/packages/config/src/forkConfig/types.ts +++ b/packages/config/src/forkConfig/types.ts @@ -35,4 +35,6 @@ export interface IForkConfig { getExecutionForkTypes(slot: Slot): allForks.AllForksExecutionSSZTypes; /** Get blinded SSZ types by hard-fork */ getBlindedForkTypes(slot: Slot): allForks.AllForksBlindedSSZTypes; + /** Get blobs SSZ tyoes by hard-fork*/ + getBlobsForkTypes(slot: Slot): allForks.AllForksBlobsSSZTypes; } diff --git a/packages/params/src/forkName.ts b/packages/params/src/forkName.ts index 6ccbe0d90892..da6c9f70a1a3 100644 --- a/packages/params/src/forkName.ts +++ b/packages/params/src/forkName.ts @@ -20,4 +20,12 @@ export enum ForkSeq { eip4844 = 4, } -export type ForkExecution = ForkName.bellatrix | ForkName.capella | ForkName.eip4844; +export type ForkExecution = Exclude; +export function isForkExecution(fork: ForkName): fork is ForkExecution { + return fork !== ForkName.phase0 && fork !== ForkName.altair; +} + +export type ForkBlobs = Exclude; +export function isForkBlobs(fork: ForkName): fork is ForkBlobs { + return isForkExecution(fork) && fork !== ForkName.bellatrix && fork !== ForkName.capella; +} diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 4c4f53146176..4862d73ce280 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -6,7 +6,7 @@ import {presetStatus} from "./presetStatus.js"; import {userSelectedPreset, userOverrides} from "./setPreset.js"; export {BeaconPreset} from "./interface.js"; -export {ForkName, ForkSeq, ForkExecution} from "./forkName.js"; +export {ForkName, ForkSeq, ForkExecution, ForkBlobs, isForkExecution, isForkBlobs} from "./forkName.js"; export {presetToJson} from "./json.js"; export {PresetName}; diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index 04c93fc32736..c4482f47815f 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -4,9 +4,10 @@ import { MAX_WITHDRAWALS_PER_PAYLOAD, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP, } from "@lodestar/params"; +import {byteArrayEquals, toHexString} from "@chainsafe/ssz"; import {CachedBeaconStateCapella} from "../types.js"; -import {decreaseBalance, hasEth1WithdrawalCredential} from "../util/index.js"; +import {decreaseBalance, hasEth1WithdrawalCredential, isCapellaPayloadHeader} from "../util/index.js"; export function processWithdrawals( state: CachedBeaconStateCapella, @@ -15,14 +16,30 @@ export function processWithdrawals( const {withdrawals: expectedWithdrawals} = getExpectedWithdrawals(state); const numWithdrawals = expectedWithdrawals.length; - if (expectedWithdrawals.length !== payload.withdrawals.length) { - throw Error(`Invalid withdrawals length expected=${numWithdrawals} actual=${payload.withdrawals.length}`); + if (isCapellaPayloadHeader(payload)) { + const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(expectedWithdrawals); + const actualWithdrawalsRoot = payload.withdrawalsRoot; + if (!byteArrayEquals(expectedWithdrawalsRoot, actualWithdrawalsRoot)) { + throw Error( + `Invalid withdrawalsRoot of executionPayloadHeader, expected=${toHexString( + expectedWithdrawalsRoot + )}, actual=${toHexString(actualWithdrawalsRoot)}` + ); + } + } else { + if (expectedWithdrawals.length !== payload.withdrawals.length) { + throw Error(`Invalid withdrawals length expected=${numWithdrawals} actual=${payload.withdrawals.length}`); + } + for (let i = 0; i < numWithdrawals; i++) { + const withdrawal = expectedWithdrawals[i]; + if (!ssz.capella.Withdrawal.equals(withdrawal, payload.withdrawals[i])) { + throw Error(`Withdrawal mismatch at index=${i}`); + } + } } + for (let i = 0; i < numWithdrawals; i++) { const withdrawal = expectedWithdrawals[i]; - if (!ssz.capella.Withdrawal.equals(withdrawal, payload.withdrawals[i])) { - throw Error(`Withdrawal mismatch at index=${i}`); - } decreaseBalance(state, withdrawal.validatorIndex, Number(withdrawal.amount)); } diff --git a/packages/state-transition/src/util/execution.ts b/packages/state-transition/src/util/execution.ts index c55cade6a28c..44cc843a17e4 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -110,7 +110,7 @@ export function isExecutionPayload( export function isCapellaPayload( payload: allForks.FullOrBlindedExecutionPayload -): payload is capella.ExecutionPayload | capella.ExecutionPayloadHeader | capella.BlindedExecutionPayload { +): payload is capella.FullOrBlindedExecutionPayload { return ( (payload as capella.ExecutionPayload).withdrawals !== undefined || (payload as capella.ExecutionPayloadHeader).withdrawalsRoot !== undefined @@ -118,7 +118,7 @@ export function isCapellaPayload( } export function isCapellaPayloadHeader( - payload: capella.ExecutionPayload | capella.ExecutionPayloadHeader | capella.BlindedExecutionPayload + payload: capella.FullOrBlindedExecutionPayload ): payload is capella.ExecutionPayloadHeader { return (payload as capella.ExecutionPayloadHeader).withdrawalsRoot !== undefined; } diff --git a/packages/types/src/allForks/sszTypes.ts b/packages/types/src/allForks/sszTypes.ts index 352d7196c438..bff6e24369a0 100644 --- a/packages/types/src/allForks/sszTypes.ts +++ b/packages/types/src/allForks/sszTypes.ts @@ -58,6 +58,8 @@ export const allForksExecution = { BeaconState: bellatrix.BeaconState, ExecutionPayload: bellatrix.ExecutionPayload, ExecutionPayloadHeader: bellatrix.ExecutionPayloadHeader, + BuilderBid: bellatrix.BuilderBid, + SignedBuilderBid: bellatrix.SignedBuilderBid, }, capella: { BeaconBlockBody: capella.BeaconBlockBody, @@ -67,6 +69,8 @@ export const allForksExecution = { // Not used in phase0 but added for type consitency ExecutionPayload: capella.ExecutionPayload, ExecutionPayloadHeader: capella.ExecutionPayloadHeader, + BuilderBid: capella.BuilderBid, + SignedBuilderBid: capella.SignedBuilderBid, }, eip4844: { BeaconBlockBody: eip4844.BeaconBlockBody, @@ -75,6 +79,8 @@ export const allForksExecution = { BeaconState: eip4844.BeaconState, ExecutionPayload: eip4844.ExecutionPayload, ExecutionPayloadHeader: eip4844.ExecutionPayloadHeader, + BuilderBid: eip4844.BuilderBid, + SignedBuilderBid: eip4844.SignedBuilderBid, }, }; @@ -99,3 +105,9 @@ export const allForksBlinded = { SignedBeaconBlock: eip4844.SignedBlindedBeaconBlock, }, }; + +export const allForksBlobs = { + eip4844: { + SignedBeaconBlockAndBlobsSidecar: eip4844.SignedBeaconBlockAndBlobsSidecar, + }, +}; diff --git a/packages/types/src/allForks/types.ts b/packages/types/src/allForks/types.ts index 9b2ce1f4298b..c4b0b97cf511 100644 --- a/packages/types/src/allForks/types.ts +++ b/packages/types/src/allForks/types.ts @@ -48,10 +48,6 @@ export type ExecutionPayloadHeader = | bellatrix.ExecutionPayloadHeader | capella.ExecutionPayloadHeader | eip4844.ExecutionPayloadHeader; -export type BlindedExecutionPayload = - | bellatrix.ExecutionPayloadHeader - | capella.BlindedExecutionPayload - | eip4844.BlindedExecutionPayload; // Blinded types that will change across forks export type BlindedBeaconBlockBody = @@ -72,6 +68,10 @@ export type FullOrBlindedBeaconBlockBody = BeaconBlockBody | BlindedBeaconBlockB export type FullOrBlindedBeaconBlock = BeaconBlock | BlindedBeaconBlock; export type FullOrBlindedSignedBeaconBlock = SignedBeaconBlock | SignedBlindedBeaconBlock; +export type BuilderBid = bellatrix.BuilderBid | capella.BuilderBid | eip4844.BuilderBid; +export type SignedBuilderBid = bellatrix.SignedBuilderBid | capella.SignedBuilderBid | eip4844.SignedBuilderBid; + +export type SignedBeaconBlockAndBlobsSidecar = eip4844.SignedBeaconBlockAndBlobsSidecar; /** * Types known to change between forks */ @@ -83,6 +83,9 @@ export type AllForksTypes = { Metadata: Metadata; ExecutionPayload: ExecutionPayload; ExecutionPayloadHeader: ExecutionPayloadHeader; + BuilderBid: BuilderBid; + SignedBuilderBid: SignedBuilderBid; + SignedBeaconBlockAndBlobsSidecar: SignedBeaconBlockAndBlobsSidecar; }; export type AllForksBlindedTypes = { @@ -173,6 +176,12 @@ export type AllForksExecutionSSZTypes = { | typeof capellaSsz.ExecutionPayloadHeader | typeof eip4844Ssz.ExecutionPayloadHeader >; + BuilderBid: AllForksTypeOf< + typeof bellatrixSsz.BuilderBid | typeof capellaSsz.BuilderBid | typeof eip4844Ssz.BuilderBid + >; + SignedBuilderBid: AllForksTypeOf< + typeof bellatrixSsz.SignedBuilderBid | typeof capellaSsz.SignedBuilderBid | typeof eip4844Ssz.SignedBuilderBid + >; }; export type AllForksBlindedSSZTypes = { @@ -190,3 +199,7 @@ export type AllForksBlindedSSZTypes = { | typeof eip4844Ssz.SignedBlindedBeaconBlock >; }; + +export type AllForksBlobsSSZTypes = { + SignedBeaconBlockAndBlobsSidecar: AllForksTypeOf; +}; diff --git a/packages/types/src/capella/sszTypes.ts b/packages/types/src/capella/sszTypes.ts index 39258878ca7a..3ad637455531 100644 --- a/packages/types/src/capella/sszTypes.ts +++ b/packages/types/src/capella/sszTypes.ts @@ -15,6 +15,7 @@ const { BLSPubkey, ExecutionAddress, Gwei, + UintBn256, } = primitiveSsz; export const Withdrawal = new ContainerType( @@ -53,14 +54,6 @@ export const ExecutionPayload = new ContainerType( {typeName: "ExecutionPayload", jsonCase: "eth2"} ); -export const BlindedExecutionPayload = new ContainerType( - { - ...bellatrixSsz.ExecutionPayloadHeader.fields, - withdrawals: Withdrawals, // New in capella - }, - {typeName: "BlindedExecutionPayload", jsonCase: "eth2"} -); - export const ExecutionPayloadHeader = new ContainerType( { ...bellatrixSsz.ExecutionPayloadHeader.fields, @@ -99,6 +92,23 @@ export const SignedBeaconBlock = new ContainerType( {typeName: "SignedBeaconBlock", jsonCase: "eth2"} ); +export const BuilderBid = new ContainerType( + { + header: ExecutionPayloadHeader, + value: UintBn256, + pubkey: BLSPubkey, + }, + {typeName: "BuilderBid", jsonCase: "eth2"} +); + +export const SignedBuilderBid = new ContainerType( + { + message: BuilderBid, + signature: BLSSignature, + }, + {typeName: "SignedBuilderBid", jsonCase: "eth2"} +); + export const HistoricalSummary = new ContainerType( { blockSummaryRoot: Root, @@ -158,7 +168,7 @@ export const BeaconState = new ContainerType( export const BlindedBeaconBlockBody = new ContainerType( { ...altairSsz.BeaconBlockBody.fields, - executionPayloadHeader: BlindedExecutionPayload, // Modified in capella + executionPayloadHeader: ExecutionPayloadHeader, // Modified in capella blsToExecutionChanges: BLSToExecutionChanges, // New in capella }, {typeName: "BlindedBeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} diff --git a/packages/types/src/capella/types.ts b/packages/types/src/capella/types.ts index 634bf71aec42..2239a6d3537b 100644 --- a/packages/types/src/capella/types.ts +++ b/packages/types/src/capella/types.ts @@ -7,7 +7,6 @@ export type BLSToExecutionChanges = ValueOf; export type SignedBLSToExecutionChange = ValueOf; export type ExecutionPayload = ValueOf; -export type BlindedExecutionPayload = ValueOf; export type ExecutionPayloadHeader = ValueOf; export type BeaconBlockBody = ValueOf; @@ -19,4 +18,7 @@ export type BlindedBeaconBlockBody = ValueOf; export type BlindedBeaconBlock = ValueOf; export type SignedBlindedBeaconBlock = ValueOf; -export type FullOrBlindedExecutionPayload = ExecutionPayload | BlindedExecutionPayload; +export type FullOrBlindedExecutionPayload = ExecutionPayload | ExecutionPayloadHeader; + +export type BuilderBid = ValueOf; +export type SignedBuilderBid = ValueOf; diff --git a/packages/types/src/eip4844/sszTypes.ts b/packages/types/src/eip4844/sszTypes.ts index 19f7ec38f132..8b535f166aa9 100644 --- a/packages/types/src/eip4844/sszTypes.ts +++ b/packages/types/src/eip4844/sszTypes.ts @@ -12,7 +12,7 @@ import {ssz as altairSsz} from "../altair/index.js"; import {ssz as capellaSsz} from "../capella/index.js"; import {ssz as bellatrixSsz} from "../bellatrix/index.js"; -const {UintNum64, Slot, Root, BLSSignature, UintBn256, Bytes32, Bytes48, Bytes96} = primitiveSsz; +const {UintNum64, Slot, Root, BLSSignature, UintBn256, Bytes32, Bytes48, Bytes96, BLSPubkey} = primitiveSsz; // Polynomial commitments // https://github.com/ethereum/consensus-specs/blob/dev/specs/eip4844/polynomial-commitments.md @@ -93,17 +93,6 @@ export const ExecutionPayload = new ContainerType( {typeName: "ExecutionPayload", jsonCase: "eth2"} ); -export const BlindedExecutionPayload = new ContainerType( - { - ...bellatrixSsz.CommonExecutionPayloadType.fields, - excessDataGas: UintBn256, // New in EIP-4844 - blockHash: Root, - transactionsRoot: Root, - withdrawalsRoot: Root, - }, - {typeName: "BlindedExecutionPayload", jsonCase: "eth2"} -); - export const ExecutionPayloadHeader = new ContainerType( { ...bellatrixSsz.CommonExecutionPayloadType.fields, @@ -164,6 +153,7 @@ export const BlindedBeaconBlockBody = new ContainerType( { ...BeaconBlockBody.fields, executionPayloadHeader: ExecutionPayloadHeader, // Modified in EIP-4844 + blobKzgCommitments: BlobKzgCommitments, // New in EIP-4844 }, {typeName: "BlindedBeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -184,6 +174,24 @@ export const SignedBlindedBeaconBlock = new ContainerType( {typeName: "SignedBlindedBeaconBlock", jsonCase: "eth2"} ); +export const BuilderBid = new ContainerType( + { + header: ExecutionPayloadHeader, + value: UintBn256, + pubkey: BLSPubkey, + blobKzgCommitments: BlobKzgCommitments, + }, + {typeName: "BuilderBid", jsonCase: "eth2"} +); + +export const SignedBuilderBid = new ContainerType( + { + message: BuilderBid, + signature: BLSSignature, + }, + {typeName: "SignedBuilderBid", jsonCase: "eth2"} +); + // We don't spread capella.BeaconState fields since we need to replace // latestExecutionPayloadHeader and we cannot keep order doing that export const BeaconState = new ContainerType( diff --git a/packages/types/src/eip4844/types.ts b/packages/types/src/eip4844/types.ts index 396311310cc4..b185d1afad0c 100644 --- a/packages/types/src/eip4844/types.ts +++ b/packages/types/src/eip4844/types.ts @@ -15,7 +15,6 @@ export type BlobsSidecarsByRangeRequest = ValueOf; export type ExecutionPayload = ValueOf; -export type BlindedExecutionPayload = ValueOf; export type ExecutionPayloadHeader = ValueOf; export type BeaconBlockBody = ValueOf; @@ -28,3 +27,8 @@ export type BeaconState = ValueOf; export type BlindedBeaconBlockBody = ValueOf; export type BlindedBeaconBlock = ValueOf; export type SignedBlindedBeaconBlock = ValueOf; + +export type FullOrBlindedExecutionPayload = ExecutionPayload | ExecutionPayloadHeader; + +export type BuilderBid = ValueOf; +export type SignedBuilderBid = ValueOf; diff --git a/packages/types/src/sszTypes.ts b/packages/types/src/sszTypes.ts index 8b0c245e5407..de120240d687 100644 --- a/packages/types/src/sszTypes.ts +++ b/packages/types/src/sszTypes.ts @@ -9,3 +9,4 @@ import {ssz as allForksSsz} from "./allForks/index.js"; export const allForks = allForksSsz.allForks; export const allForksBlinded = allForksSsz.allForksBlinded; export const allForksExecution = allForksSsz.allForksExecution; +export const allForksBlobs = allForksSsz.allForksBlobs; diff --git a/packages/types/src/utils/typeguards.ts b/packages/types/src/utils/typeguards.ts index 39cc6219be80..c918915a47d7 100644 --- a/packages/types/src/utils/typeguards.ts +++ b/packages/types/src/utils/typeguards.ts @@ -1,6 +1,17 @@ -import {FullOrBlindedBeaconBlock, FullOrBlindedSignedBeaconBlock} from "../allForks/types.js"; +import { + FullOrBlindedBeaconBlock, + FullOrBlindedSignedBeaconBlock, + FullOrBlindedExecutionPayload, + ExecutionPayloadHeader, +} from "../allForks/types.js"; import {ts as bellatrix} from "../bellatrix/index.js"; +export function isBlindedExecution(payload: FullOrBlindedExecutionPayload): payload is ExecutionPayloadHeader { + // we just check transactionsRoot for determinging as it the base field + // that is present and differs from ExecutionPayload for all forks + return (payload as ExecutionPayloadHeader).transactionsRoot !== undefined; +} + export function isBlindedBeaconBlock(block: FullOrBlindedBeaconBlock): block is bellatrix.BlindedBeaconBlock { return (block as bellatrix.BlindedBeaconBlock).body.executionPayloadHeader !== undefined; }