From 24f3bf8c99cb369595ba2578e23a57d6ef4831d1 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Mon, 2 Feb 2026 14:11:38 +0100 Subject: [PATCH 01/34] feat: implement epbs block production --- .../api/src/beacon/routes/beacon/block.ts | 105 ++++++-- packages/api/src/beacon/routes/validator.ts | 147 ++++++++++- packages/api/src/utils/fork.ts | 10 + .../api/test/unit/beacon/testData/beacon.ts | 4 + .../test/unit/beacon/testData/validator.ts | 26 ++ .../src/api/impl/beacon/blocks/index.ts | 111 +++++++- .../src/api/impl/validator/index.ts | 126 +++++++++ .../chain/produceBlock/produceBlockBody.ts | 241 +++++++++++++++++- packages/beacon-node/src/network/interface.ts | 2 + packages/beacon-node/src/network/network.ts | 12 + packages/beacon-node/src/util/dataColumns.ts | 24 ++ packages/validator/src/services/block.ts | 84 ++++++ .../validator/src/services/validatorStore.ts | 41 +++ 13 files changed, 907 insertions(+), 26 deletions(-) diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index 29354d3c19f6..c296ad3df024 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -8,6 +8,7 @@ import { ForkPreElectra, isForkPostBellatrix, isForkPostDeneb, + isForkPostGloas, } from "@lodestar/params"; import { BeaconBlockBody, @@ -17,11 +18,12 @@ import { SignedBlockContents, Slot, deneb, + gloas, ssz, sszTypesFor, } from "@lodestar/types"; import {EmptyMeta, EmptyResponseCodec, EmptyResponseData, WithVersion} from "../../../utils/codecs.js"; -import {getPostBellatrixForkTypes, toForkName} from "../../../utils/fork.js"; +import {getPostBellatrixForkTypes, getPostGloasForkTypes, toForkName} from "../../../utils/fork.js"; import {fromHeaders} from "../../../utils/headers.js"; import {Endpoint, RequestCodec, RouteDefinitions, Schema} from "../../../utils/index.js"; import { @@ -209,6 +211,20 @@ export type Endpoints = { EmptyMeta >; + /** + * Publish signed execution payload envelope. + * Instructs the beacon node to broadcast a signed execution payload envelope to the network, + * to be gossiped for payload validation. A success response (20x) indicates that the envelope + * passed gossip validation and was successfully broadcast onto the network. + */ + publishExecutionPayloadEnvelope: Endpoint< + "POST", + {signedExecutionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope}, + {body: unknown; headers: {[MetaHeader.Version]: string}}, + EmptyResponseData, + EmptyMeta + >; + /** * Get block BlobSidecar * Retrieves BlobSidecar included in requested block. @@ -406,11 +422,14 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions) - : sszTypesFor(fork).SignedBeaconBlock.toJson( - signedBlockContents.signedBlock as SignedBeaconBlock - ), + body: + isForkPostDeneb(fork) && !isForkPostGloas(fork) + ? sszTypesFor(fork).SignedBlockContents.toJson( + signedBlockContents as SignedBlockContents + ) + : sszTypesFor(fork).SignedBeaconBlock.toJson( + signedBlockContents.signedBlock as SignedBeaconBlock + ), headers: { [MetaHeader.Version]: fork, }, @@ -420,9 +439,10 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { const forkName = toForkName(fromHeaders(headers, MetaHeader.Version)); return { - signedBlockContents: isForkPostDeneb(forkName) - ? sszTypesFor(forkName).SignedBlockContents.fromJson(body) - : {signedBlock: ssz[forkName].SignedBeaconBlock.fromJson(body)}, + signedBlockContents: + isForkPostDeneb(forkName) && !isForkPostGloas(forkName) + ? sszTypesFor(forkName).SignedBlockContents.fromJson(body) + : {signedBlock: ssz[forkName].SignedBeaconBlock.fromJson(body)}, broadcastValidation: query.broadcast_validation as BroadcastValidation, }; }, @@ -431,13 +451,14 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions - ) - : sszTypesFor(fork).SignedBeaconBlock.serialize( - signedBlockContents.signedBlock as SignedBeaconBlock - ), + body: + isForkPostDeneb(fork) && !isForkPostGloas(fork) + ? sszTypesFor(fork).SignedBlockContents.serialize( + signedBlockContents as SignedBlockContents + ) + : sszTypesFor(fork).SignedBeaconBlock.serialize( + signedBlockContents.signedBlock as SignedBeaconBlock + ), headers: { [MetaHeader.Version]: fork, }, @@ -447,9 +468,10 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { const forkName = toForkName(fromHeaders(headers, MetaHeader.Version)); return { - signedBlockContents: isForkPostDeneb(forkName) - ? sszTypesFor(forkName).SignedBlockContents.deserialize(body) - : {signedBlock: ssz[forkName].SignedBeaconBlock.deserialize(body)}, + signedBlockContents: + isForkPostDeneb(forkName) && !isForkPostGloas(forkName) + ? sszTypesFor(forkName).SignedBlockContents.deserialize(body) + : {signedBlock: ssz[forkName].SignedBeaconBlock.deserialize(body)}, broadcastValidation: query.broadcast_validation as BroadcastValidation, }; }, @@ -566,6 +588,51 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { + const fork = config.getForkName(signedExecutionPayloadEnvelope.message.slot); + return { + body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.toJson(signedExecutionPayloadEnvelope), + headers: { + [MetaHeader.Version]: fork, + }, + }; + }, + parseReqJson: ({body, headers}) => { + const fork = toForkName(fromHeaders(headers, MetaHeader.Version)); + return { + signedExecutionPayloadEnvelope: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.fromJson(body), + }; + }, + writeReqSsz: ({signedExecutionPayloadEnvelope}) => { + const fork = config.getForkName(signedExecutionPayloadEnvelope.message.slot); + return { + body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.serialize(signedExecutionPayloadEnvelope), + headers: { + [MetaHeader.Version]: fork, + }, + }; + }, + parseReqSsz: ({body, headers}) => { + const fork = toForkName(fromHeaders(headers, MetaHeader.Version)); + return { + signedExecutionPayloadEnvelope: + getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.deserialize(body), + }; + }, + schema: { + body: Schema.Object, + headers: {[MetaHeader.Version]: Schema.String}, + }, + }, + resp: EmptyResponseCodec, + init: { + requestWireFormat: WireFormat.ssz, + }, + }, getBlobSidecars: { url: "/eth/v1/beacon/blob_sidecars/{block_id}", method: "GET", diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 32a94959a71d..c2e3196a6ef8 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -2,6 +2,7 @@ import {ContainerType, Type, ValueOf} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import { ForkPostDeneb, + ForkPostGloas, ForkPreDeneb, VALIDATOR_REGISTRY_LIMIT, isForkPostDeneb, @@ -22,6 +23,7 @@ import { UintBn64, ValidatorIndex, altair, + gloas, phase0, ssz, sszTypesFor, @@ -36,7 +38,7 @@ import { JsonOnlyReq, WithVersion, } from "../../utils/codecs.js"; -import {getPostBellatrixForkTypes, toForkName} from "../../utils/fork.js"; +import {getPostBellatrixForkTypes, getPostGloasForkTypes, toForkName} from "../../utils/fork.js"; import {fromHeaders} from "../../utils/headers.js"; import {Endpoint, RouteDefinitions, Schema} from "../../utils/index.js"; import { @@ -89,6 +91,17 @@ export type ProduceBlockV3Meta = ValueOf & { executionPayloadSource: ProducedBlockSource; }; +export const ProduceBlockV4MetaType = new ContainerType( + { + ...VersionType.fields, + /** Consensus rewards paid to the proposer for this block, in Wei */ + consensusBlockValue: ssz.UintBn64, + }, + {jsonCase: "eth2"} +); + +export type ProduceBlockV4Meta = ValueOf; + export const AttesterDutyType = new ContainerType( { /** The validator's public key, uniquely identifying them */ @@ -357,6 +370,59 @@ export type Endpoints = { ProduceBlockV3Meta >; + /** + * Requests a beacon node to produce a valid block, which can then be signed by a validator. + * + * Post-Gloas, proposers submit execution payload bids rather than full execution payloads, + * so there is no longer a concept of blinded or unblinded blocks. Builders release the payload later. + * This endpoint is specific to the post-Gloas forks and is not backwards compatible with previous forks. + */ + produceBlockV4: Endpoint< + "GET", + { + /** The slot for which the block should be proposed */ + slot: Slot; + /** The validator's randao reveal value */ + randaoReveal: BLSSignature; + /** Arbitrary data validator wants to include in block */ + graffiti?: string; + skipRandaoVerification?: boolean; + builderBoostFactor?: UintBn64; + } & Omit, + { + params: {slot: number}; + query: { + randao_reveal: string; + graffiti?: string; + skip_randao_verification?: string; + fee_recipient?: string; + builder_selection?: string; + builder_boost_factor?: string; + strict_fee_recipient_check?: boolean; + }; + }, + BeaconBlock, + ProduceBlockV4Meta + >; + + /** + * Get execution payload envelope. + * Retrieves execution payload envelope for a given slot and builder. + * The envelope contains the full execution payload along with associated metadata. + */ + getExecutionPayloadEnvelope: Endpoint< + "GET", + { + /** Slot for which the execution payload envelope is requested */ + slot: Slot; + /** Index of the builder from which the execution payload envelope is requested */ + builderIndex: ValidatorIndex; + }, + {params: {slot: Slot; builder_index: ValidatorIndex}}, + gloas.ExecutionPayloadEnvelope, + VersionMeta + >; + /** * Produce an attestation data * Requests that the beacon node produce an AttestationData. @@ -763,6 +829,85 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({ + params: {slot}, + query: { + randao_reveal: toHex(randaoReveal), + graffiti: toGraffitiHex(graffiti), + skip_randao_verification: writeSkipRandaoVerification(skipRandaoVerification), + fee_recipient: feeRecipient, + builder_selection: builderSelection, + builder_boost_factor: builderBoostFactor?.toString(), + strict_fee_recipient_check: strictFeeRecipientCheck, + }, + }), + parseReq: ({params, query}) => ({ + slot: params.slot, + randaoReveal: fromHex(query.randao_reveal), + graffiti: fromGraffitiHex(query.graffiti), + skipRandaoVerification: parseSkipRandaoVerification(query.skip_randao_verification), + feeRecipient: query.fee_recipient, + builderSelection: query.builder_selection as BuilderSelection, + builderBoostFactor: parseBuilderBoostFactor(query.builder_boost_factor), + strictFeeRecipientCheck: query.strict_fee_recipient_check, + }), + schema: { + params: {slot: Schema.UintRequired}, + query: { + randao_reveal: Schema.StringRequired, + graffiti: Schema.String, + skip_randao_verification: Schema.String, + fee_recipient: Schema.String, + builder_selection: Schema.String, + builder_boost_factor: Schema.String, + strict_fee_recipient_check: Schema.Boolean, + }, + }, + }, + resp: { + data: WithVersion((fork) => getPostGloasForkTypes(fork).BeaconBlock), + meta: { + toJson: (meta) => ProduceBlockV4MetaType.toJson(meta), + fromJson: (val) => ProduceBlockV4MetaType.fromJson(val), + toHeadersObject: (meta) => ({ + [MetaHeader.Version]: meta.version, + [MetaHeader.ConsensusBlockValue]: meta.consensusBlockValue.toString(), + }), + fromHeaders: (headers) => ({ + version: toForkName(headers.getRequired(MetaHeader.Version)), + consensusBlockValue: BigInt(headers.getRequired(MetaHeader.ConsensusBlockValue)), + }), + }, + }, + }, + getExecutionPayloadEnvelope: { + url: "/eth/v1/validator/execution_payload_envelope/{slot}/{builder_index}", + method: "GET", + req: { + writeReq: ({slot, builderIndex}) => ({params: {slot, builder_index: builderIndex}}), + parseReq: ({params}) => ({slot: params.slot, builderIndex: params.builder_index}), + schema: { + params: {slot: Schema.UintRequired, builder_index: Schema.UintRequired}, + }, + }, + resp: { + data: ssz.gloas.ExecutionPayloadEnvelope, + meta: VersionCodec, + }, + }, produceAttestationData: { url: "/eth/v1/validator/attestation_data", method: "GET", diff --git a/packages/api/src/utils/fork.ts b/packages/api/src/utils/fork.ts index a2f4428cce6f..978d4f590d39 100644 --- a/packages/api/src/utils/fork.ts +++ b/packages/api/src/utils/fork.ts @@ -3,9 +3,11 @@ import { ForkPostAltair, ForkPostBellatrix, ForkPostDeneb, + ForkPostGloas, isForkPostAltair, isForkPostBellatrix, isForkPostDeneb, + isForkPostGloas, } from "@lodestar/params"; import {SSZTypesFor, sszTypesFor} from "@lodestar/types"; @@ -42,3 +44,11 @@ export function getPostDenebForkTypes(fork: ForkName): SSZTypesFor { + if (!isForkPostGloas(fork)) { + throw Error(`Invalid fork=${fork} for post-gloas fork types`); + } + + return sszTypesFor(fork); +} diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 49fa3e0d8d0b..f4e18e546765 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -91,6 +91,10 @@ export const testData: GenericServerTestCases = { }, res: undefined, }, + publishExecutionPayloadEnvelope: { + args: {signedExecutionPayloadEnvelope: ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue()}, + res: undefined, + }, getBlobSidecars: { args: {blockId: "head", indices: [0]}, res: { diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index bba342cb3f56..2654bf1d5597 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -72,6 +72,32 @@ export const testData: GenericServerTestCases = { }, }, }, + produceBlockV4: { + args: { + slot: 32000, + randaoReveal, + graffiti, + skipRandaoVerification: true, + builderBoostFactor: 0n, + feeRecipient, + builderSelection: BuilderSelection.ExecutionAlways, + strictFeeRecipientCheck: true, + }, + res: { + data: ssz.gloas.BeaconBlock.defaultValue(), + meta: { + version: ForkName.gloas, + consensusBlockValue: ssz.Wei.defaultValue(), + }, + }, + }, + getExecutionPayloadEnvelope: { + args: {slot: 32000, builderIndex: 1}, + res: { + data: ssz.gloas.ExecutionPayloadEnvelope.defaultValue(), + meta: {version: ForkName.gloas}, + }, + }, produceAttestationData: { args: {committeeIndex: 2, slot: 32000}, res: {data: ssz.phase0.AttestationData.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 02817128d306..bf3f95c61225 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -1,6 +1,7 @@ import {routes} from "@lodestar/api"; import {ApiError, ApplicationMethods} from "@lodestar/api/server"; import { + BUILDER_INDEX_SELF_BUILD, ForkPostBellatrix, ForkPostFulu, ForkPreGloas, @@ -42,6 +43,7 @@ import { ProduceFullBellatrix, ProduceFullDeneb, ProduceFullFulu, + ProduceFullGloas, } from "../../../../chain/produceBlock/index.js"; import {validateGossipBlock} from "../../../../chain/validation/block.js"; import {OpSource} from "../../../../chain/validatorMonitor.js"; @@ -51,7 +53,7 @@ import { kzgCommitmentToVersionedHash, reconstructBlobs, } from "../../../../util/blobs.js"; -import {getDataColumnSidecarsFromBlock} from "../../../../util/dataColumns.js"; +import {getDataColumnSidecarsForGloas, getDataColumnSidecarsFromBlock} from "../../../../util/dataColumns.js"; import {isOptimisticBlock} from "../../../../util/forkChoice.js"; import {kzg} from "../../../../util/kzg.js"; import {promiseAllMaybeAsync} from "../../../../util/promises.js"; @@ -93,6 +95,7 @@ export function getBeaconBlockApi({ const fork = config.getForkName(slot); const blockRoot = toRootHex(chain.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(signedBlock.message)); + // TODO GLOAS: handle new BlockInput type const blockForImport = chain.seenBlockInputCache.getByBlock({ block: signedBlock, source: BlockInputSource.api, @@ -299,7 +302,7 @@ export function getBeaconBlockApi({ ]; const sentPeersArr = await promiseAllMaybeAsync(publishPromises); - if (isForkPostFulu(fork)) { + if (isForkPostFulu(fork) && !isForkPostGloas(fork)) { let columnsPublishedWithZeroPeers = 0; // sent peers per topic are logged in network.publishGossip(), here we only track metrics for it // starting from fulu, we have to push to 128 subnets so need to make sure we have enough sent peers per topic @@ -622,6 +625,110 @@ export function getBeaconBlockApi({ await publishBlock(args, context, opts); }, + async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}) { + const seenTimestampSec = Date.now() / 1000; + const envelope = signedExecutionPayloadEnvelope.message; + const slot = envelope.slot; + const fork = config.getForkName(slot); + const blockRootHex = toRootHex(envelope.beaconBlockRoot); + + if (!isForkPostGloas(fork)) { + throw new ApiError(400, `publishExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`); + } + + // Validate that we're not too far in the future + const currentSlot = chain.clock.currentSlot; + if (slot > currentSlot + 1) { + throw new ApiError(400, `Envelope slot ${slot} is too far in the future (current: ${currentSlot})`); + } + + const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; + let dataColumnSidecars: fulu.DataColumnSidecars = []; + + // For self-builds, retrieve cached production data and build DataColumnSidecars + if (isSelfBuild) { + const cachedResult = chain.blockProductionCache.get(blockRootHex) as ProduceFullGloas | undefined; + if (cachedResult?.cells && cachedResult.cellProofs && cachedResult.blobKzgCommitments.length > 0) { + // Get the signed block header from the block we produced + const blockResult = await chain.getBlockByRoot(blockRootHex); + if (blockResult) { + const signedBlockHeader = signedBlockToSignedHeader(config, blockResult.block); + + // Build cellsAndProofs from the cached cells and proofs + // cellProofs is a flat array: 128 proofs per blob + const cellsAndProofs = cachedResult.cells.map((rowCells, rowIndex) => ({ + cells: rowCells, + proofs: cachedResult.cellProofs.slice(rowIndex * NUMBER_OF_COLUMNS, (rowIndex + 1) * NUMBER_OF_COLUMNS), + })); + + // Build DataColumnSidecars for GLOAS (commitments from envelope) + dataColumnSidecars = getDataColumnSidecarsForGloas( + signedBlockHeader, + cachedResult.blobKzgCommitments, + cellsAndProofs + ); + } + } + } + + // TODO GLOAS: Verify execution payload envelope signature + // For self-builds with G2_POINT_AT_INFINITY signature, this is a no-op + // For external builders, verify using verify_execution_payload_envelope_signature(state, signed_envelope) + + // TODO GLOAS: Process execution payload via state transition + // Call process_execution_payload(state, signed_envelope, execution_engine) + + // TODO GLOAS: Update fork choice with the execution payload + // Call on_execution_payload(store, signed_envelope) to update fork choice state + + const valLogMeta = { + slot, + blockRoot: blockRootHex, + builderIndex: envelope.builderIndex, + isSelfBuild, + dataColumns: dataColumnSidecars.length, + }; + + chain.logger.info("Publishing execution payload envelope", valLogMeta); + + // Publish envelope and data columns + const publishPromises = [ + // Gossip the signed execution payload envelope first + () => network.publishSignedExecutionPayloadEnvelope(signedExecutionPayloadEnvelope), + // For self-builds, publish all DataColumnSidecars + ...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)), + ]; + + const sentPeersArr = await promiseAllMaybeAsync(publishPromises); + + // Track metrics for data column publishing + if (dataColumnSidecars.length > 0) { + let columnsPublishedWithZeroPeers = 0; + // Skip first entry (envelope), track data columns + for (let i = 1; i < sentPeersArr.length; i++) { + const sentPeers = sentPeersArr[i] as number; + metrics?.dataColumns.sentPeersPerSubnet.observe(sentPeers); + if (sentPeers === 0) { + columnsPublishedWithZeroPeers++; + } + } + if (columnsPublishedWithZeroPeers > 0) { + chain.logger.warn("Published data columns to 0 peers for GLOAS envelope", { + slot, + blockRoot: blockRootHex, + columns: columnsPublishedWithZeroPeers, + }); + } + } + + const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime); + chain.logger.info("Published execution payload envelope", { + ...valLogMeta, + delaySec, + sentPeers: (sentPeersArr[0] as number) ?? 0, + }); + }, + async getBlobSidecars({blockId, indices}) { assertUniqueItems(indices, "Duplicate indices provided"); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 2affb5db9f7b..92dfd5bef88e 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -3,6 +3,7 @@ import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; import {ExecutionStatus, ProtoBlock} from "@lodestar/fork-choice"; import { + BUILDER_INDEX_SELF_BUILD, ForkName, ForkPostBellatrix, ForkPreGloas, @@ -14,6 +15,7 @@ import { isForkPostBellatrix, isForkPostDeneb, isForkPostElectra, + isForkPostGloas, } from "@lodestar/params"; import { CachedBeaconStateAllForks, @@ -45,6 +47,7 @@ import { Wei, bellatrix, getValidatorStatus, + gloas, phase0, ssz, } from "@lodestar/types"; @@ -900,6 +903,52 @@ export function getValidatorApi( return {data, meta}; }, + async produceBlockV4({slot, randaoReveal, graffiti, feeRecipient}) { + const fork = config.getForkName(slot); + + if (!isForkPostGloas(fork)) { + throw new ApiError(400, `produceBlockV4 not supported for pre-gloas fork=${fork}`); + } + + notWhileSyncing(); + await waitForSlot(slot); + + const parentBlock = chain.getProposerHead(slot); + + // Build a common block body for later processes + const graffitiBytes = toGraffitiBytes( + graffiti ?? getDefaultGraffiti(getLodestarClientVersion(), chain.executionEngine.clientVersion, {}) + ); + const commonBlockBodyPromise = chain.produceCommonBlockBody({ + slot, + parentBlock, + randaoReveal, + graffiti: graffitiBytes, + }); + + // For gloas, we return a beacon block with separate execution payload bid + const {block, consensusBlockValue} = await chain.produceBlock({ + slot, + parentBlock, + randaoReveal, + graffiti: graffitiBytes, + feeRecipient, + commonBlockBodyPromise, + }); + + const version = config.getForkName(block.slot); + + metrics?.blockProductionSuccess.inc({source: ProducedBlockSource.engine}); + + return { + data: block as gloas.BeaconBlock, + meta: { + version, + consensusBlockValue, + }, + }; + }, + async produceAttestationData({committeeIndex, slot}) { notWhileSyncing(); @@ -1531,5 +1580,82 @@ export function getValidatorApi( count: filteredRegistrations.length, }); }, + + async getExecutionPayloadEnvelope({slot, builderIndex}) { + notWhileSyncing(); + await waitForSlot(slot); + + const fork = config.getForkName(slot); + if (!isForkPostGloas(fork)) { + throw new ApiError(400, `getExecutionPayloadEnvelope not supported for fork=${fork}`); + } + + // For self-builds, the proposer is also the builder + const proposerIndex = chain.getHeadState().epochCtx.getBeaconProposer(slot); + if (builderIndex !== proposerIndex) { + // TODO GLOAS: For external builders, fetch envelope via P2P or builder API + throw new ApiError( + 404, + `Builder index ${builderIndex} does not match proposer ${proposerIndex} for self-build` + ); + } + + // Search the block production cache for a block produced at this slot + // The cache is keyed by block root, so we need to search through entries + let cachedResult: { + blockRootHex: string; + executionPayload: gloas.ExecutionPayloadEnvelope["payload"]; + executionRequests: gloas.ExecutionPayloadEnvelope["executionRequests"]; + blobKzgCommitments: gloas.ExecutionPayloadEnvelope["blobKzgCommitments"]; + } | null = null; + + for (const [blockRootHex, produceResult] of chain.blockProductionCache.entries()) { + if (produceResult.fork === fork && "executionPayload" in produceResult) { + const gloasResult = produceResult as { + fork: typeof fork; + executionPayload: gloas.ExecutionPayloadEnvelope["payload"]; + executionRequests: gloas.ExecutionPayloadEnvelope["executionRequests"]; + blobKzgCommitments: gloas.ExecutionPayloadEnvelope["blobKzgCommitments"]; + }; + // Check if the payload matches the requested slot + if (gloasResult.executionPayload && Number(gloasResult.executionPayload.timestamp) > 0) { + // For self-builds at this slot, this should be our produced block + cachedResult = { + blockRootHex, + ...gloasResult, + }; + break; + } + } + } + + if (!cachedResult) { + throw new ApiError(404, `No cached block production result found for slot ${slot}`); + } + + // Build the envelope + // For self-builds, builder_index = BUILDER_INDEX_SELF_BUILD (UINT64_MAX) + const beaconBlockRoot = fromHex(cachedResult.blockRootHex); + + const envelope: gloas.ExecutionPayloadEnvelope = { + payload: cachedResult.executionPayload, + executionRequests: cachedResult.executionRequests, + builderIndex: BUILDER_INDEX_SELF_BUILD, + beaconBlockRoot, + slot, + blobKzgCommitments: cachedResult.blobKzgCommitments, + // TODO GLOAS: Compute state root properly - for now use zero hash + // The state root should be computed by calling process_execution_payload with verify=false + // and then taking hash_tree_root(state) as per the builder spec + stateRoot: new Uint8Array(32), + }; + + return { + data: envelope, + meta: { + version: fork, + }, + }; + }, }; } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index b386065231e0..577f66a48bcc 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -1,10 +1,12 @@ import {ChainForkConfig} from "@lodestar/config"; import {ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; import { + BUILDER_INDEX_SELF_BUILD, ForkName, ForkPostBellatrix, ForkPostDeneb, ForkPostFulu, + ForkPostGloas, ForkPreGloas, ForkSeq, isForkPostAltair, @@ -16,6 +18,8 @@ import { CachedBeaconStateBellatrix, CachedBeaconStateCapella, CachedBeaconStateExecutions, + CachedBeaconStateGloas, + G2_POINT_AT_INFINITY, computeTimeAtSlot, getExpectedWithdrawals, getRandaoMix, @@ -42,6 +46,8 @@ import { deneb, electra, fulu, + gloas, + ssz, } from "@lodestar/types"; import {Logger, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {ZERO_HASH_HEX} from "../../constants/index.js"; @@ -99,6 +105,16 @@ export type AssembledBodyType = T extends BlockType.Full : BlindedBeaconBlockBody; export type AssembledBlockType = T extends BlockType.Full ? BeaconBlock : BlindedBeaconBlock; +export type ProduceFullGloas = { + type: BlockType.Full; + fork: ForkPostGloas; + executionPayload: ExecutionPayload; + executionRequests: electra.ExecutionRequests; + blobKzgCommitments: deneb.BlobKzgCommitments; + cells: fulu.Cell[][]; + /** Cell proofs for building DataColumnSidecars, 128 proofs per blob */ + cellProofs: Uint8Array[]; +}; export type ProduceFullFulu = { type: BlockType.Full; fork: ForkPostFulu; @@ -131,6 +147,7 @@ export type ProduceBlinded = { /** The result of local block production, everything that's not the block itself */ export type ProduceResult = + | ProduceFullGloas | ProduceFullFulu | ProduceFullDeneb | ProduceFullBellatrix @@ -180,12 +197,123 @@ export async function produceBlockBody( this.logger.verbose("Producing beacon block body", logMeta); if (isForkPostGloas(fork)) { - // TODO GLOAS: Set body.signedExecutionPayloadBid and body.payloadAttestation + // Post-Gloas block production: get execution payload from EL, create self-build bid + const gloasState = currentState as CachedBeaconStateGloas; + const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); + const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; + const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex); + + const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer(); + + this.logger.verbose("Preparing Gloas execution payload from engine", { + slot: blockSlot, + parentBlockRoot: toRootHex(parentBlockRoot), + feeRecipient, + }); + + // Get execution payload from EL (similar to pre-Gloas, but we don't include it in block body) + const prepareRes = await prepareExecutionPayloadGloas( + this, + this.logger, + fork as ForkPostGloas, + parentBlockRoot, + safeBlockHash, + finalizedBlockHash ?? ZERO_HASH_HEX, + gloasState, + feeRecipient + ); + + const {prepType, payloadId} = prepareRes; + Object.assign(logMeta, {executionPayloadPrepType: prepType}); + + if (prepType !== PayloadPreparationType.Cached) { + await sleep(PAYLOAD_GENERATION_TIME_MS); + } + + this.logger.verbose("Fetching Gloas execution payload from engine", {slot: blockSlot, payloadId}); + const payloadRes = await this.executionEngine.getPayload(fork, payloadId); + + endExecutionPayload?.({step: BlockProductionStep.executionPayload}); + + const {executionPayload, blobsBundle, executionRequests} = payloadRes; + executionPayloadValue = payloadRes.executionPayloadValue; + shouldOverrideBuilder = payloadRes.shouldOverrideBuilder; + + if (blobsBundle === undefined) { + throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`); + } + if (executionRequests === undefined) { + throw Error(`Missing executionRequests response from getPayload at fork=${fork}`); + } + + // Compute cells for PeerDAS + const cells = blobsBundle.blobs.map((blob) => kzg.computeCells(blob)); + if (this.opts.sanityCheckExecutionEngineBlobs) { + await validateCellsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, cells); + } + + // Compute blobKzgCommitmentsRoot for the bid + const blobKzgCommitmentsRoot = ssz.deneb.BlobKzgCommitments.hashTreeRoot(blobsBundle.commitments); + + // Create self-build execution payload bid + // For self-builds, builder_index = BUILDER_INDEX_SELF_BUILD (UINT64_MAX) + // and value/executionPayment are 0 + const bid: gloas.ExecutionPayloadBid = { + parentBlockHash: gloasState.latestBlockHash, + parentBlockRoot: parentBlockRoot, + blockHash: executionPayload.blockHash, + prevRandao: getRandaoMix(gloasState, gloasState.epochCtx.epoch), + feeRecipient: executionPayload.feeRecipient, + gasLimit: BigInt(executionPayload.gasLimit), + builderIndex: BUILDER_INDEX_SELF_BUILD, + slot: blockSlot, + value: 0, + executionPayment: 0, + blobKzgCommitmentsRoot, + }; + + // For self-builds, signature is G2_POINT_AT_INFINITY + const signedBid: gloas.SignedExecutionPayloadBid = { + message: bid, + signature: G2_POINT_AT_INFINITY, + }; + + // Get common block body and add Gloas-specific fields const commonBlockBody = await commonBlockBodyPromise; - blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType; - executionPayloadValue = BigInt(0); + const gloasBody = Object.assign({}, commonBlockBody) as gloas.BeaconBlockBody; + gloasBody.signedExecutionPayloadBid = signedBid; + + // TODO GLOAS: Get payload attestations from pool for previous slot + // For now, set empty payload attestations + gloasBody.payloadAttestations = []; + + blockBody = gloasBody as AssembledBodyType; + + // Store execution payload data in produceResult for envelope creation + (produceResult as ProduceFullGloas).executionPayload = executionPayload as ExecutionPayload; + (produceResult as ProduceFullGloas).executionRequests = executionRequests; + (produceResult as ProduceFullGloas).blobKzgCommitments = blobsBundle.commitments; + (produceResult as ProduceFullGloas).cells = cells; + // Store cell proofs for building DataColumnSidecars during envelope publishing + (produceResult as ProduceFullGloas).cellProofs = blobsBundle.proofs; + + const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime); + this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime); + this.logger.verbose("Produced Gloas block with self-build bid", { + slot: blockSlot, + executionPayloadValue, + prepType, + payloadId, + fetchedTime, + executionBlockHash: toRootHex(executionPayload.blockHash), + blobs: blobsBundle.commitments.length, + }); - // We don't deal with blinded blocks, execution engine, blobs and execution requests post-gloas + Object.assign(logMeta, { + transactions: executionPayload.transactions.length, + blobs: blobsBundle.commitments.length, + shouldOverrideBuilder, + }); } else if (isForkPostBellatrix(fork)) { const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; @@ -542,6 +670,73 @@ export async function prepareExecutionPayload( return {payloadId, prepType}; } +/** + * Produce ExecutionPayload for Gloas. + * Similar to prepareExecutionPayload but uses latestBlockHash instead of latestExecutionPayloadHeader. + */ +export async function prepareExecutionPayloadGloas( + chain: { + executionEngine: IExecutionEngine; + config: ChainForkConfig; + }, + logger: Logger, + fork: ForkPostGloas, + parentBlockRoot: Root, + safeBlockHash: RootHex, + finalizedBlockHash: RootHex, + state: CachedBeaconStateGloas, + suggestedFeeRecipient: string +): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { + // Gloas uses latestBlockHash instead of latestExecutionPayloadHeader.blockHash + const parentHash = state.latestBlockHash; + const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime); + const prevRandao = getRandaoMix(state, state.epochCtx.epoch); + + const payloadIdCached = chain.executionEngine.payloadIdCache.get({ + headBlockHash: toRootHex(parentHash), + finalizedBlockHash, + timestamp: numToQuantity(timestamp), + prevRandao: toHex(prevRandao), + suggestedFeeRecipient, + }); + + let payloadId: PayloadId | null; + let prepType: PayloadPreparationType; + + if (payloadIdCached) { + payloadId = payloadIdCached; + prepType = PayloadPreparationType.Cached; + } else { + if (chain.executionEngine.payloadIdCache.hasPayload({timestamp: numToQuantity(timestamp)})) { + prepType = PayloadPreparationType.Reorged; + } else { + prepType = PayloadPreparationType.Fresh; + } + + const attributes: PayloadAttributes = preparePayloadAttributesGloas(fork, chain, { + prepareState: state, + prepareSlot: state.slot, + parentBlockRoot, + feeRecipient: suggestedFeeRecipient, + }); + + payloadId = await chain.executionEngine.notifyForkchoiceUpdate( + fork, + toRootHex(parentHash), + safeBlockHash, + finalizedBlockHash, + attributes + ); + logger.verbose("Prepared Gloas payload id from execution engine", {payloadId}); + } + + if (payloadId === null) { + throw Error("notifyForkchoiceUpdate returned payloadId null"); + } + + return {payloadId, prepType}; +} + async function prepareExecutionPayloadHeader( chain: { executionBuilder?: IExecutionBuilder; @@ -634,6 +829,44 @@ function preparePayloadAttributes( return payloadAttributes; } +/** + * Prepare payload attributes for Gloas forks. + * Similar to preparePayloadAttributes but uses Gloas state structure. + */ +function preparePayloadAttributesGloas( + fork: ForkPostGloas, + chain: { + config: ChainForkConfig; + }, + { + prepareState, + prepareSlot, + parentBlockRoot, + feeRecipient, + }: { + prepareState: CachedBeaconStateGloas; + prepareSlot: Slot; + parentBlockRoot: Root; + feeRecipient: string; + } +): PayloadAttributes { + const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime); + const prevRandao = getRandaoMix(prepareState, prepareState.epochCtx.epoch); + + // Get withdrawals using the same withdrawal logic as electra + const {expectedWithdrawals} = getExpectedWithdrawals(ForkSeq[fork], prepareState); + + const payloadAttributes: PayloadAttributes = { + timestamp, + prevRandao, + suggestedFeeRecipient: feeRecipient, + withdrawals: expectedWithdrawals, + parentBeaconBlockRoot: parentBlockRoot, + }; + + return payloadAttributes; +} + export async function produceCommonBlockBody( this: BeaconChain, blockType: T, diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index 347f94f04156..33e3de1669f8 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -31,6 +31,7 @@ import { capella, deneb, fulu, + gloas, phase0, } from "@lodestar/types"; import {BlockInputSource} from "../chain/blocks/blockInput/types.js"; @@ -95,6 +96,7 @@ export interface INetwork extends INetworkCorePublic { publishContributionAndProof(contributionAndProof: altair.SignedContributionAndProof): Promise; publishLightClientFinalityUpdate(update: LightClientFinalityUpdate): Promise; publishLightClientOptimisticUpdate(update: LightClientOptimisticUpdate): Promise; + publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise; // Debug dumpGossipQueue(gossipType: GossipType): Promise; diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 6169102af4d5..8f1d8aad8583 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -24,6 +24,7 @@ import { capella, deneb, fulu, + gloas, phase0, } from "@lodestar/types"; import {prettyPrintIndices, sleep} from "@lodestar/utils"; @@ -489,6 +490,17 @@ export class Network implements INetwork { ); } + async publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise { + const epoch = computeEpochAtSlot(signedEnvelope.message.slot); + const boundary = this.config.getForkBoundaryAtEpoch(epoch); + + return this.publishGossip( + {type: GossipType.execution_payload, boundary}, + signedEnvelope, + {ignoreDuplicatePublishError: true} + ); + } + private async publishGossip( topic: GossipTopicMap[K], object: GossipTypeMap[K], diff --git a/packages/beacon-node/src/util/dataColumns.ts b/packages/beacon-node/src/util/dataColumns.ts index 0a498275ec5a..830b9421ea15 100644 --- a/packages/beacon-node/src/util/dataColumns.ts +++ b/packages/beacon-node/src/util/dataColumns.ts @@ -346,6 +346,30 @@ export function getDataColumnSidecarsFromColumnSidecar( ); } +/** + * For GLOAS self-builds: build DataColumnSidecars from the envelope data. + * In GLOAS, blobKzgCommitments are in the ExecutionPayloadEnvelope, not the BeaconBlockBody. + * The inclusion proof is computed against the envelope's blobKzgCommitments field. + */ +export function getDataColumnSidecarsForGloas( + signedBlockHeader: SignedBeaconBlockHeader, + blobKzgCommitments: deneb.BlobKzgCommitments, + cellsAndKzgProofs: {cells: Uint8Array[]; proofs: Uint8Array[]}[] +): fulu.DataColumnSidecars { + // No need to create data column sidecars if there are no blobs + if (blobKzgCommitments.length === 0) { + return []; + } + + // For GLOAS, the inclusion proof is for the envelope's blobKzgCommitments field + // The envelope structure has a different gindex than the block body + // TODO GLOAS: Compute proper inclusion proof for envelope's blobKzgCommitments + // For now, use an empty proof since the envelope already contains the commitments explicitly + const kzgCommitmentsInclusionProof = new Array(17).fill(new Uint8Array(32)) as fulu.KzgCommitmentsInclusionProof; + + return getDataColumnSidecars(signedBlockHeader, blobKzgCommitments, kzgCommitmentsInclusionProof, cellsAndKzgProofs); +} + /** * If we receive more than half of NUMBER_OF_COLUMNS (64) we should recover all remaining columns */ diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index 69923a71ad99..541bef64ca2e 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -1,5 +1,6 @@ import {ApiClient, routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; +import {isForkPostGloas} from "@lodestar/params"; import { BLSPubkey, BLSSignature, @@ -95,6 +96,13 @@ export class BlockProposingService { // Wrap with try catch here to re-use `logCtx` try { + const fork = this.config.getForkName(slot); + + // Gloas uses different block production flow + if (isForkPostGloas(fork)) { + return this.createAndPublishBlockGloas(pubkey, slot); + } + const randaoReveal = await this.validatorStore.signRandao(pubkey, slot); const graffiti = this.validatorStore.getGraffiti(pubkeyHex); @@ -163,6 +171,82 @@ export class BlockProposingService { } } + /** + * Gloas block production flow: + * 1. Produce beacon block with execution payload bid + * 2. Sign and publish the beacon block + * 3. Get the execution payload envelope + * 4. Sign and publish the envelope + */ + private async createAndPublishBlockGloas(pubkey: BLSPubkey, slot: Slot): Promise { + const pubkeyHex = toPubkeyHex(pubkey); + const debugLogCtx = {slot, validator: pubkeyHex}; + + const randaoReveal = await this.validatorStore.signRandao(pubkey, slot); + const graffiti = this.validatorStore.getGraffiti(pubkeyHex); + const feeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex); + + this.logger.debug("Producing Gloas block", {...debugLogCtx, feeRecipient}); + this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot)); + + // Step 1: Produce beacon block with execution payload bid + const blockRes = await this.api.validator.produceBlockV4({ + slot, + randaoReveal, + graffiti, + feeRecipient, + }); + const block = blockRes.value(); + const blockMeta = blockRes.meta(); + + this.logger.debug("Produced Gloas block", { + ...debugLogCtx, + consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue), + }); + this.metrics?.blocksProduced.inc(); + + // Step 2: Sign and publish the beacon block + const signedBlock = await this.validatorStore.signBlock(pubkey, block, slot, this.logger); + + const {broadcastValidation} = this.opts; + await this.api.beacon.publishBlockV2({ + signedBlockContents: {signedBlock}, + broadcastValidation, + }); + + this.logger.debug("Published Gloas beacon block", debugLogCtx); + + // Step 3: Get the execution payload envelope + // For self-builds, the builder index is the proposer's validator index + const validatorIndex = this.validatorStore.getValidatorIndex(pubkeyHex); + if (validatorIndex === undefined) { + throw new Error(`Validator index not found for ${pubkeyHex}`); + } + + const envelopeRes = await this.api.validator.getExecutionPayloadEnvelope({ + slot, + builderIndex: validatorIndex, + }); + const envelope = envelopeRes.value(); + + this.logger.debug("Retrieved execution payload envelope", debugLogCtx); + + // Step 4: Sign and publish the envelope + const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(pubkey, envelope, slot); + + await this.api.beacon.publishExecutionPayloadEnvelope({ + signedExecutionPayloadEnvelope: signedEnvelope, + }); + + this.metrics?.proposerStepCallPublishBlock.observe(this.clock.secFromSlot(slot)); + this.metrics?.blocksPublished.inc(); + this.logger.info("Published Gloas block and envelope", { + ...debugLogCtx, + graffiti, + consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue), + }); + } + private publishBlockWrapper = async ( signedBlindedBlockOrBlockContents: SignedBlockContents | {signedBlock: SignedBlindedBeaconBlock}, opts: {broadcastValidation?: routes.beacon.BroadcastValidation} = {} diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 6f24ef6738a0..d2a59cd1c409 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -6,6 +6,7 @@ import { DOMAIN_AGGREGATE_AND_PROOF, DOMAIN_APPLICATION_BUILDER, DOMAIN_BEACON_ATTESTER, + DOMAIN_BEACON_BUILDER, DOMAIN_BEACON_PROPOSER, DOMAIN_CONTRIBUTION_AND_PROOF, DOMAIN_RANDAO, @@ -39,6 +40,7 @@ import { ValidatorIndex, altair, bellatrix, + gloas, phase0, ssz, } from "@lodestar/types"; @@ -212,6 +214,10 @@ export class ValidatorStore { return this.indicesService.index2pubkey.get(index); } + getValidatorIndex(pubkeyHex: PubkeyHex): ValidatorIndex | undefined { + return this.indicesService.getValidatorIndex(pubkeyHex); + } + pollValidatorIndices(): Promise { // Consumers will call this function every epoch forever. If everyone has been discovered, skip return this.indicesService.indexCount >= this.validators.size @@ -487,6 +493,41 @@ export class ValidatorStore { } as SignedBeaconBlock | SignedBlindedBeaconBlock; } + /** + * Sign an execution payload envelope for Gloas self-building. + * Uses DOMAIN_BEACON_BUILDER domain as per the spec. + */ + async signExecutionPayloadEnvelope( + pubkey: BLSPubkey, + envelope: gloas.ExecutionPayloadEnvelope, + currentSlot: Slot + ): Promise { + // Make sure the envelope slot is not higher than the current slot to avoid potential attacks. + if (envelope.slot > currentSlot) { + throw Error(`Not signing envelope with slot ${envelope.slot} greater than current slot ${currentSlot}`); + } + + // Duties are filtered before-hard by doppelganger-safe, this assert should never throw + this.assertDoppelgangerSafe(pubkey); + + const signingSlot = envelope.slot; + const domain = this.config.getDomain(signingSlot, DOMAIN_BEACON_BUILDER); + const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadEnvelope, envelope, domain); + + // TODO GLOAS: Add slashing protection for envelope signing if needed + // For self-builds, this is similar to block proposal protection + + const signableMessage: SignableMessage = { + type: SignableMessageType.BLOCK_V2, // TODO GLOAS: Add dedicated type for envelope signing + data: envelope as unknown as BeaconBlock, + }; + + return { + message: envelope, + signature: await this.getSignature(pubkey, signingRoot, signingSlot, signableMessage), + }; + } + async signRandao(pubkey: BLSPubkey, slot: Slot): Promise { const signingSlot = slot; const domain = this.config.getDomain(slot, DOMAIN_RANDAO); From ddd4339d1e8ae09dd937b59c37b25250ba9f1b0c Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sun, 8 Feb 2026 13:34:49 +0000 Subject: [PATCH 02/34] wip --- .../beacon/genericServerTest/beacon.test.ts | 12 +++- .../api/test/unit/beacon/testData/beacon.ts | 14 ++-- .../src/api/impl/validator/index.ts | 72 ++++++++++--------- .../validator/src/services/validatorStore.ts | 7 +- .../src/util/externalSignerClient.ts | 9 ++- 5 files changed, 71 insertions(+), 43 deletions(-) diff --git a/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts b/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts index 941f6fcb5a8a..a57cc519266e 100644 --- a/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts @@ -8,7 +8,17 @@ import {testData} from "../testData/beacon.js"; describe("beacon / beacon", () => { runGenericServerTest( - createChainForkConfig({...defaultChainConfig, ELECTRA_FORK_EPOCH: 0}), + // TODO: revisit + createChainForkConfig({ + ...defaultChainConfig, + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, + FULU_FORK_EPOCH: 0, + GLOAS_FORK_EPOCH: 2, + }), getClient, getRoutes, testData diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index f4e18e546765..eb4b86128efc 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -91,10 +91,16 @@ export const testData: GenericServerTestCases = { }, res: undefined, }, - publishExecutionPayloadEnvelope: { - args: {signedExecutionPayloadEnvelope: ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue()}, - res: undefined, - }, + publishExecutionPayloadEnvelope: (() => { + // TODO: revisit + const envelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue(); + // Slot must be in the gloas epoch (GLOAS_FORK_EPOCH * SLOTS_PER_EPOCH) + envelope.message.slot = 32000; + return { + args: {signedExecutionPayloadEnvelope: envelope}, + res: undefined, + }; + })(), getBlobSidecars: { args: {blockId: "head", indices: [0]}, res: { diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 2473d8e9748f..6094e69bcaa2 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -19,7 +19,9 @@ import { } from "@lodestar/params"; import { CachedBeaconStateAllForks, + CachedBeaconStateGloas, DataAvailabilityStatus, + StateHashTreeRootSource, attesterShufflingDecisionRoot, beaconBlockToBlinded, calculateCommitteeAssignments, @@ -32,6 +34,7 @@ import { loadState, proposerShufflingDecisionRoot, } from "@lodestar/state-transition"; +import {processExecutionPayloadEnvelope} from "@lodestar/state-transition/block"; import { BLSSignature, BeaconBlock, @@ -72,7 +75,7 @@ import { } from "../../../chain/errors/index.js"; import {ChainEvent, CommonBlockBody} from "../../../chain/index.js"; import {PREPARE_NEXT_SLOT_BPS} from "../../../chain/prepareNextSlot.js"; -import {BlockType, ProduceFullDeneb} from "../../../chain/produceBlock/index.js"; +import {BlockType, ProduceFullDeneb, ProduceFullGloas} from "../../../chain/produceBlock/index.js"; import {RegenCaller} from "../../../chain/regen/index.js"; import {CheckpointHex} from "../../../chain/stateCache/types.js"; import {validateApiAggregateAndProof} from "../../../chain/validation/index.js"; @@ -1592,7 +1595,8 @@ export function getValidatorApi( } // For self-builds, the proposer is also the builder - const proposerIndex = chain.getHeadState().epochCtx.getBeaconProposer(slot); + const headState = chain.getHeadState(); + const proposerIndex = headState.epochCtx.getBeaconProposer(slot); if (builderIndex !== proposerIndex) { // TODO GLOAS: For external builders, fetch envelope via P2P or builder API throw new ApiError( @@ -1603,30 +1607,12 @@ export function getValidatorApi( // Search the block production cache for a block produced at this slot // The cache is keyed by block root, so we need to search through entries - let cachedResult: { - blockRootHex: string; - executionPayload: gloas.ExecutionPayloadEnvelope["payload"]; - executionRequests: gloas.ExecutionPayloadEnvelope["executionRequests"]; - blobKzgCommitments: gloas.ExecutionPayloadEnvelope["blobKzgCommitments"]; - } | null = null; + let cachedResult: {blockRootHex: string; produceResult: ProduceFullGloas} | null = null; for (const [blockRootHex, produceResult] of chain.blockProductionCache.entries()) { - if (produceResult.fork === fork && "executionPayload" in produceResult) { - const gloasResult = produceResult as { - fork: typeof fork; - executionPayload: gloas.ExecutionPayloadEnvelope["payload"]; - executionRequests: gloas.ExecutionPayloadEnvelope["executionRequests"]; - blobKzgCommitments: gloas.ExecutionPayloadEnvelope["blobKzgCommitments"]; - }; - // Check if the payload matches the requested slot - if (gloasResult.executionPayload && Number(gloasResult.executionPayload.timestamp) > 0) { - // For self-builds at this slot, this should be our produced block - cachedResult = { - blockRootHex, - ...gloasResult, - }; - break; - } + if (produceResult.fork === fork && produceResult.type === BlockType.Full && "executionPayload" in produceResult) { + cachedResult = {blockRootHex, produceResult: produceResult as ProduceFullGloas}; + break; } } @@ -1634,23 +1620,45 @@ export function getValidatorApi( throw new ApiError(404, `No cached block production result found for slot ${slot}`); } + const {blockRootHex, produceResult} = cachedResult; + // Build the envelope // For self-builds, builder_index = BUILDER_INDEX_SELF_BUILD (UINT64_MAX) - const beaconBlockRoot = fromHex(cachedResult.blockRootHex); + const beaconBlockRoot = fromHex(blockRootHex); const envelope: gloas.ExecutionPayloadEnvelope = { - payload: cachedResult.executionPayload, - executionRequests: cachedResult.executionRequests, + payload: produceResult.executionPayload, + executionRequests: produceResult.executionRequests, builderIndex: BUILDER_INDEX_SELF_BUILD, beaconBlockRoot, slot, - blobKzgCommitments: cachedResult.blobKzgCommitments, - // TODO GLOAS: Compute state root properly - for now use zero hash - // The state root should be computed by calling process_execution_payload with verify=false - // and then taking hash_tree_root(state) as per the builder spec - stateRoot: new Uint8Array(32), + blobKzgCommitments: produceResult.blobKzgCommitments, + stateRoot: ZERO_HASH, }; + // Compute state root by running processExecutionPayloadEnvelope with verify=false + // as per the builder spec, then taking hash_tree_root(state) + if (headState.slot !== slot) { + chain.logger.warn("Head state slot mismatch for envelope state root computation, using zero hash", { + headSlot: headState.slot, + requestedSlot: slot, + }); + } else { + const postBlockState = headState.clone(true) as CachedBeaconStateGloas; + const tempSignedEnvelope: gloas.SignedExecutionPayloadEnvelope = { + message: envelope, + signature: new Uint8Array(96), + }; + + processExecutionPayloadEnvelope(postBlockState, tempSignedEnvelope, false); + + const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ + source: StateHashTreeRootSource.computeNewStateRoot, + }); + envelope.stateRoot = postBlockState.hashTreeRoot(); + hashTreeRootTimer?.(); + } + return { data: envelope, meta: { diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index d2a59cd1c409..05d3626e6557 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -514,12 +514,9 @@ export class ValidatorStore { const domain = this.config.getDomain(signingSlot, DOMAIN_BEACON_BUILDER); const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadEnvelope, envelope, domain); - // TODO GLOAS: Add slashing protection for envelope signing if needed - // For self-builds, this is similar to block proposal protection - const signableMessage: SignableMessage = { - type: SignableMessageType.BLOCK_V2, // TODO GLOAS: Add dedicated type for envelope signing - data: envelope as unknown as BeaconBlock, + type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE, + data: envelope, }; return { diff --git a/packages/validator/src/util/externalSignerClient.ts b/packages/validator/src/util/externalSignerClient.ts index 9a74f47740fd..8f062f419392 100644 --- a/packages/validator/src/util/externalSignerClient.ts +++ b/packages/validator/src/util/externalSignerClient.ts @@ -11,6 +11,7 @@ import { RootHex, Slot, altair, + gloas, phase0, ssz, sszTypesFor, @@ -32,6 +33,7 @@ export enum SignableMessageType { SYNC_COMMITTEE_SELECTION_PROOF = "SYNC_COMMITTEE_SELECTION_PROOF", SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF = "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF", VALIDATOR_REGISTRATION = "VALIDATOR_REGISTRATION", + EXECUTION_PAYLOAD_ENVELOPE = "EXECUTION_PAYLOAD_ENVELOPE", } const AggregationSlotType = new ContainerType({ @@ -80,7 +82,8 @@ export type SignableMessage = | {type: SignableMessageType.SYNC_COMMITTEE_MESSAGE; data: ValueOf} | {type: SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF; data: ValueOf} | {type: SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF; data: altair.ContributionAndProof} - | {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1}; + | {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1} + | {type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE; data: gloas.ExecutionPayloadEnvelope}; const requiresForkInfo: Record = { [SignableMessageType.AGGREGATION_SLOT]: true, @@ -95,6 +98,7 @@ const requiresForkInfo: Record = { [SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF]: true, [SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF]: true, [SignableMessageType.VALIDATOR_REGISTRATION]: false, + [SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE]: true, }; type Web3SignerSerializedRequest = { @@ -266,6 +270,9 @@ function serializerSignableMessagePayload(config: BeaconConfig, payload: Signabl case SignableMessageType.VALIDATOR_REGISTRATION: return {validator_registration: ssz.bellatrix.ValidatorRegistrationV1.toJson(payload.data)}; + + case SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE: + return {execution_payload_envelope: ssz.gloas.ExecutionPayloadEnvelope.toJson(payload.data)}; } } From 096fda84cd8cb89543b2871887ea4cebb9fc00c1 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 12 Feb 2026 13:04:16 +0000 Subject: [PATCH 03/34] Fix lint --- packages/beacon-node/src/api/impl/validator/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 6094e69bcaa2..f79113225a61 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1610,7 +1610,11 @@ export function getValidatorApi( let cachedResult: {blockRootHex: string; produceResult: ProduceFullGloas} | null = null; for (const [blockRootHex, produceResult] of chain.blockProductionCache.entries()) { - if (produceResult.fork === fork && produceResult.type === BlockType.Full && "executionPayload" in produceResult) { + if ( + produceResult.fork === fork && + produceResult.type === BlockType.Full && + "executionPayload" in produceResult + ) { cachedResult = {blockRootHex, produceResult: produceResult as ProduceFullGloas}; break; } From 1871373a83623248d09032325e9bcaa926707cd6 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 13 Feb 2026 12:09:16 +0000 Subject: [PATCH 04/34] Review packages/api --- packages/api/src/beacon/routes/beacon/block.ts | 5 +++-- .../beacon/genericServerTest/beacon.test.ts | 12 +----------- .../api/test/unit/beacon/testData/beacon.ts | 18 +++++++----------- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index c296ad3df024..2450478d0b5f 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -3,6 +3,7 @@ import {ChainForkConfig} from "@lodestar/config"; import { ForkName, ForkPostDeneb, + ForkPostGloas, ForkPreBellatrix, ForkPreDeneb, ForkPreElectra, @@ -428,7 +429,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ) : sszTypesFor(fork).SignedBeaconBlock.toJson( - signedBlockContents.signedBlock as SignedBeaconBlock + signedBlockContents.signedBlock as SignedBeaconBlock ), headers: { [MetaHeader.Version]: fork, @@ -457,7 +458,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ) : sszTypesFor(fork).SignedBeaconBlock.serialize( - signedBlockContents.signedBlock as SignedBeaconBlock + signedBlockContents.signedBlock as SignedBeaconBlock ), headers: { [MetaHeader.Version]: fork, diff --git a/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts b/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts index a57cc519266e..4692ae296df2 100644 --- a/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts @@ -8,17 +8,7 @@ import {testData} from "../testData/beacon.js"; describe("beacon / beacon", () => { runGenericServerTest( - // TODO: revisit - createChainForkConfig({ - ...defaultChainConfig, - ALTAIR_FORK_EPOCH: 0, - BELLATRIX_FORK_EPOCH: 0, - CAPELLA_FORK_EPOCH: 0, - DENEB_FORK_EPOCH: 0, - ELECTRA_FORK_EPOCH: 0, - FULU_FORK_EPOCH: 0, - GLOAS_FORK_EPOCH: 2, - }), + createChainForkConfig({...defaultChainConfig, GLOAS_FORK_EPOCH: 0}), getClient, getRoutes, testData diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index eb4b86128efc..56ea109eae67 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -75,7 +75,9 @@ export const testData: GenericServerTestCases = { }, publishBlockV2: { args: { - signedBlockContents: ssz.electra.SignedBlockContents.defaultValue(), + signedBlockContents: { + signedBlock: ssz.gloas.SignedBeaconBlock.defaultValue(), + }, broadcastValidation: BroadcastValidation.consensus, }, res: undefined, @@ -91,16 +93,10 @@ export const testData: GenericServerTestCases = { }, res: undefined, }, - publishExecutionPayloadEnvelope: (() => { - // TODO: revisit - const envelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue(); - // Slot must be in the gloas epoch (GLOAS_FORK_EPOCH * SLOTS_PER_EPOCH) - envelope.message.slot = 32000; - return { - args: {signedExecutionPayloadEnvelope: envelope}, - res: undefined, - }; - })(), + publishExecutionPayloadEnvelope: { + args: {signedExecutionPayloadEnvelope: ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue()}, + res: undefined, + }, getBlobSidecars: { args: {blockId: "head", indices: [0]}, res: { From be868f3fa94ccb328ed4813536431fddf2c81ba7 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 13 Feb 2026 12:13:54 +0000 Subject: [PATCH 05/34] Updates for alpha.2 spec --- .../beacon-node/src/chain/produceBlock/produceBlockBody.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 577f66a48bcc..8b13afb5a189 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -252,9 +252,6 @@ export async function produceBlockBody( await validateCellsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, cells); } - // Compute blobKzgCommitmentsRoot for the bid - const blobKzgCommitmentsRoot = ssz.deneb.BlobKzgCommitments.hashTreeRoot(blobsBundle.commitments); - // Create self-build execution payload bid // For self-builds, builder_index = BUILDER_INDEX_SELF_BUILD (UINT64_MAX) // and value/executionPayment are 0 @@ -269,7 +266,7 @@ export async function produceBlockBody( slot: blockSlot, value: 0, executionPayment: 0, - blobKzgCommitmentsRoot, + blobKzgCommitments: blobsBundle.commitments, }; // For self-builds, signature is G2_POINT_AT_INFINITY From daec1900763cdf7265ac5146cd195a48855bff52 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 13 Feb 2026 12:17:27 +0000 Subject: [PATCH 06/34] Formatting --- packages/api/test/unit/beacon/testData/beacon.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 56ea109eae67..b470e98cbe03 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -75,9 +75,7 @@ export const testData: GenericServerTestCases = { }, publishBlockV2: { args: { - signedBlockContents: { - signedBlock: ssz.gloas.SignedBeaconBlock.defaultValue(), - }, + signedBlockContents: {signedBlock: ssz.gloas.SignedBeaconBlock.defaultValue()}, broadcastValidation: BroadcastValidation.consensus, }, res: undefined, From 2a1c870cef3ff72b308c639612173cc7c0d9b0a2 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 13 Feb 2026 12:25:32 +0000 Subject: [PATCH 07/34] Fix build --- packages/beacon-node/src/api/impl/validator/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index f79113225a61..11a97d32d02a 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1636,7 +1636,6 @@ export function getValidatorApi( builderIndex: BUILDER_INDEX_SELF_BUILD, beaconBlockRoot, slot, - blobKzgCommitments: produceResult.blobKzgCommitments, stateRoot: ZERO_HASH, }; From d0d1a37700a695cb1ad8698116b9f842698616f5 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 13 Feb 2026 13:04:29 +0000 Subject: [PATCH 08/34] Add beacon_block_root to getExecutionPayloadEnvelope --- packages/api/src/beacon/routes/validator.ts | 22 ++++++++++++++----- .../test/unit/beacon/testData/validator.ts | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index c2e3196a6ef8..e46510175860 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -415,10 +415,12 @@ export type Endpoints = { { /** Slot for which the execution payload envelope is requested */ slot: Slot; + /** Root of the beacon block that this envelope is for */ + beaconBlockRoot: Root; /** Index of the builder from which the execution payload envelope is requested */ builderIndex: ValidatorIndex; }, - {params: {slot: Slot; builder_index: ValidatorIndex}}, + {params: {slot: Slot; beacon_block_root: string; builder_index: ValidatorIndex}}, gloas.ExecutionPayloadEnvelope, VersionMeta >; @@ -894,13 +896,23 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({params: {slot, builder_index: builderIndex}}), - parseReq: ({params}) => ({slot: params.slot, builderIndex: params.builder_index}), + writeReq: ({slot, beaconBlockRoot, builderIndex}) => ({ + params: {slot, beacon_block_root: toRootHex(beaconBlockRoot), builder_index: builderIndex}, + }), + parseReq: ({params}) => ({ + slot: params.slot, + beaconBlockRoot: fromHex(params.beacon_block_root), + builderIndex: params.builder_index, + }), schema: { - params: {slot: Schema.UintRequired, builder_index: Schema.UintRequired}, + params: { + slot: Schema.UintRequired, + beacon_block_root: Schema.StringRequired, + builder_index: Schema.UintRequired, + }, }, }, resp: { diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index 2654bf1d5597..0e48b3085746 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -92,7 +92,7 @@ export const testData: GenericServerTestCases = { }, }, getExecutionPayloadEnvelope: { - args: {slot: 32000, builderIndex: 1}, + args: {slot: 32000, beaconBlockRoot: ZERO_HASH, builderIndex: 1}, res: { data: ssz.gloas.ExecutionPayloadEnvelope.defaultValue(), meta: {version: ForkName.gloas}, From d9f90a21bc2f95b9d84880ac08ed69b5201ec4aa Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 13 Feb 2026 13:10:34 +0000 Subject: [PATCH 09/34] Pass beacon block root from validator client --- packages/validator/src/services/block.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index 541bef64ca2e..e967eb22c278 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -223,8 +223,10 @@ export class BlockProposingService { throw new Error(`Validator index not found for ${pubkeyHex}`); } + const beaconBlockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block); const envelopeRes = await this.api.validator.getExecutionPayloadEnvelope({ slot, + beaconBlockRoot, builderIndex: validatorIndex, }); const envelope = envelopeRes.value(); From ba769a74d6eb112930b915fafcd80b9c52b42b18 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 13 Feb 2026 16:40:50 +0000 Subject: [PATCH 10/34] Major refactoring --- .../src/api/impl/beacon/blocks/index.ts | 37 ++-- .../src/api/impl/validator/index.ts | 98 +++------- packages/beacon-node/src/chain/chain.ts | 37 +++- packages/beacon-node/src/chain/emitter.ts | 4 +- .../beacon-node/src/chain/prepareNextSlot.ts | 10 +- .../chain/produceBlock/computeNewStateRoot.ts | 36 +++- .../chain/produceBlock/produceBlockBody.ts | 176 ++++-------------- .../src/chain/validation/dataColumnSidecar.ts | 7 +- .../src/network/gossip/interface.ts | 6 +- .../beacon-node/src/network/gossip/topic.ts | 3 +- packages/beacon-node/src/network/interface.ts | 3 +- packages/beacon-node/src/network/network.ts | 12 +- .../src/network/processor/gossipHandlers.ts | 3 +- packages/beacon-node/src/util/dataColumns.ts | 44 +++-- .../state-transition/src/stateTransition.ts | 1 + packages/types/src/types.ts | 17 +- packages/validator/src/services/block.ts | 10 +- 17 files changed, 221 insertions(+), 283 deletions(-) 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 3bf12024c0d3..8332617bd1f4 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -28,6 +28,7 @@ import { WithOptionalBytes, deneb, fulu, + gloas, isDenebBlockContents, sszTypesFor, } from "@lodestar/types"; @@ -653,37 +654,27 @@ export function getBeaconBlockApi({ } const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; - let dataColumnSidecars: fulu.DataColumnSidecars = []; + let dataColumnSidecars: gloas.DataColumnSidecars = []; // For self-builds, retrieve cached production data and build DataColumnSidecars if (isSelfBuild) { const cachedResult = chain.blockProductionCache.get(blockRootHex) as ProduceFullGloas | undefined; - if (cachedResult?.cells && cachedResult.cellProofs && cachedResult.blobKzgCommitments.length > 0) { - // Get the signed block header from the block we produced - const blockResult = await chain.getBlockByRoot(blockRootHex); - if (blockResult) { - const signedBlockHeader = signedBlockToSignedHeader(config, blockResult.block); - - // Build cellsAndProofs from the cached cells and proofs - // cellProofs is a flat array: 128 proofs per blob - const cellsAndProofs = cachedResult.cells.map((rowCells, rowIndex) => ({ - cells: rowCells, - proofs: cachedResult.cellProofs.slice(rowIndex * NUMBER_OF_COLUMNS, (rowIndex + 1) * NUMBER_OF_COLUMNS), - })); - - // Build DataColumnSidecars for GLOAS (commitments from envelope) - dataColumnSidecars = getDataColumnSidecarsForGloas( - signedBlockHeader, - cachedResult.blobKzgCommitments, - cellsAndProofs - ); - } + if (cachedResult?.cells && cachedResult.blobKzgCommitments.length > 0) { + // TODO GLOAS: cell proofs are not currently cached, need to store them during block production + // Build cellsAndProofs from the cached cells + const cellsAndProofs = cachedResult.cells.map((rowCells) => ({ + cells: rowCells, + proofs: [] as Uint8Array[], + })); + + dataColumnSidecars = getDataColumnSidecarsForGloas(slot, envelope.beaconBlockRoot, cellsAndProofs); } } // TODO GLOAS: Verify execution payload envelope signature - // For self-builds with G2_POINT_AT_INFINITY signature, this is a no-op - // For external builders, verify using verify_execution_payload_envelope_signature(state, signed_envelope) + // For self-builds, the proposer signs with their own key (NOT G2_POINT_AT_INFINITY — that's only for the bid) + // For external builders, verify using the builder's registered pubkey + // Use verify_execution_payload_envelope_signature(state, signed_envelope) // TODO GLOAS: Process execution payload via state transition // Call process_execution_payload(state, signed_envelope, execution_engine) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 11a97d32d02a..8ade8edf7f54 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -19,9 +19,7 @@ import { } from "@lodestar/params"; import { CachedBeaconStateAllForks, - CachedBeaconStateGloas, DataAvailabilityStatus, - StateHashTreeRootSource, attesterShufflingDecisionRoot, beaconBlockToBlinded, calculateCommitteeAssignments, @@ -34,7 +32,6 @@ import { loadState, proposerShufflingDecisionRoot, } from "@lodestar/state-transition"; -import {processExecutionPayloadEnvelope} from "@lodestar/state-transition/block"; import { BLSSignature, BeaconBlock, @@ -917,9 +914,13 @@ export function getValidatorApi( notWhileSyncing(); await waitForSlot(slot); + // TODO GLOAS: needs to be updated after fork choice changes are merged const parentBlock = chain.getProposerHead(slot); + const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock; + const parentBlockRoot = fromHex(parentBlockRootHex); + notOnOutOfRangeData(parentBlockRoot); + metrics?.blockProductionSlotDelta.set(slot - parentSlot); - // Build a common block body for later processes const graffitiBytes = toGraffitiBytes( graffiti ?? getDefaultGraffiti(getLodestarClientVersion(), chain.executionEngine.clientVersion, {}) ); @@ -930,7 +931,6 @@ export function getValidatorApi( graffiti: graffitiBytes, }); - // For gloas, we return a beacon block with separate execution payload bid const {block, consensusBlockValue} = await chain.produceBlock({ slot, parentBlock, @@ -940,14 +940,12 @@ export function getValidatorApi( commonBlockBodyPromise, }); - const version = config.getForkName(block.slot); - metrics?.blockProductionSuccess.inc({source: ProducedBlockSource.engine}); return { data: block as gloas.BeaconBlock, meta: { - version, + version: fork, consensusBlockValue, }, }; @@ -1585,83 +1583,45 @@ export function getValidatorApi( }); }, - async getExecutionPayloadEnvelope({slot, builderIndex}) { - notWhileSyncing(); - await waitForSlot(slot); - + async getExecutionPayloadEnvelope({slot, beaconBlockRoot, builderIndex}) { const fork = config.getForkName(slot); + if (!isForkPostGloas(fork)) { - throw new ApiError(400, `getExecutionPayloadEnvelope not supported for fork=${fork}`); + throw new ApiError(400, `getExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`); } - // For self-builds, the proposer is also the builder - const headState = chain.getHeadState(); - const proposerIndex = headState.epochCtx.getBeaconProposer(slot); - if (builderIndex !== proposerIndex) { - // TODO GLOAS: For external builders, fetch envelope via P2P or builder API - throw new ApiError( - 404, - `Builder index ${builderIndex} does not match proposer ${proposerIndex} for self-build` - ); - } + notWhileSyncing(); + await waitForSlot(slot); - // Search the block production cache for a block produced at this slot - // The cache is keyed by block root, so we need to search through entries - let cachedResult: {blockRootHex: string; produceResult: ProduceFullGloas} | null = null; - - for (const [blockRootHex, produceResult] of chain.blockProductionCache.entries()) { - if ( - produceResult.fork === fork && - produceResult.type === BlockType.Full && - "executionPayload" in produceResult - ) { - cachedResult = {blockRootHex, produceResult: produceResult as ProduceFullGloas}; - break; - } + // TODO GLOAS: add support for acting as builder + if (builderIndex !== BUILDER_INDEX_SELF_BUILD) { + throw new ApiError(400, `Builder index must be BUILDER_INDEX_SELF_BUILD but got ${builderIndex}`); } - if (!cachedResult) { - throw new ApiError(404, `No cached block production result found for slot ${slot}`); - } + const blockRootHex = toRootHex(beaconBlockRoot); + const produceResult = chain.blockProductionCache.get(blockRootHex); - const {blockRootHex, produceResult} = cachedResult; + if (produceResult === undefined) { + throw new ApiError(404, `No cached block production result found for block root ${blockRootHex}`); + } + if (!isForkPostGloas(produceResult.fork)) { + throw Error(`Cached block production result is for pre-gloas fork=${produceResult.fork}`); + } + if (produceResult.type !== BlockType.Full) { + throw Error("Cached block production result is not full block"); + } - // Build the envelope - // For self-builds, builder_index = BUILDER_INDEX_SELF_BUILD (UINT64_MAX) - const beaconBlockRoot = fromHex(blockRootHex); + const {executionPayload, executionRequests, envelopeStateRoot} = produceResult as ProduceFullGloas; const envelope: gloas.ExecutionPayloadEnvelope = { - payload: produceResult.executionPayload, - executionRequests: produceResult.executionRequests, + payload: executionPayload, + executionRequests: executionRequests, builderIndex: BUILDER_INDEX_SELF_BUILD, beaconBlockRoot, slot, - stateRoot: ZERO_HASH, + stateRoot: envelopeStateRoot, }; - // Compute state root by running processExecutionPayloadEnvelope with verify=false - // as per the builder spec, then taking hash_tree_root(state) - if (headState.slot !== slot) { - chain.logger.warn("Head state slot mismatch for envelope state root computation, using zero hash", { - headSlot: headState.slot, - requestedSlot: slot, - }); - } else { - const postBlockState = headState.clone(true) as CachedBeaconStateGloas; - const tempSignedEnvelope: gloas.SignedExecutionPayloadEnvelope = { - message: envelope, - signature: new Uint8Array(96), - }; - - processExecutionPayloadEnvelope(postBlockState, tempSignedEnvelope, false); - - const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ - source: StateHashTreeRootSource.computeNewStateRoot, - }); - envelope.stateRoot = postBlockState.hashTreeRoot(); - hashTreeRootTimer?.(); - } - return { data: envelope, meta: { diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 9c10aab69fc8..59518f716224 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -5,11 +5,19 @@ import {CompositeTypeAny, TreeView, Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; import {CheckpointWithHex, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice"; import {LoggerNode} from "@lodestar/logger/node"; -import {EFFECTIVE_BALANCE_INCREMENT, GENESIS_SLOT, SLOTS_PER_EPOCH, isForkPostElectra} from "@lodestar/params"; +import { + BUILDER_INDEX_SELF_BUILD, + EFFECTIVE_BALANCE_INCREMENT, + GENESIS_SLOT, + SLOTS_PER_EPOCH, + isForkPostElectra, + isForkPostGloas, +} from "@lodestar/params"; import { BeaconStateAllForks, BeaconStateElectra, CachedBeaconStateAllForks, + CachedBeaconStateGloas, EffectiveBalanceIncrements, EpochShuffling, Index2PubkeyCache, @@ -39,6 +47,7 @@ import { Wei, deneb, fulu, + gloas, isBlindedBeaconBlock, phase0, rewards, @@ -87,8 +96,8 @@ import { } from "./opPools/index.js"; import {IChainOptions} from "./options.js"; import {PrepareNextSlotScheduler} from "./prepareNextSlot.js"; -import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js"; -import {AssembledBlockType, BlockType, ProduceResult} from "./produceBlock/index.js"; +import {computeEnvelopeStateRoot, computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js"; +import {AssembledBlockType, BlockType, ProduceFullGloas, ProduceResult} from "./produceBlock/index.js"; import {BlockAttributes, produceBlockBody, produceCommonBlockBody} from "./produceBlock/produceBlockBody.js"; import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js"; import {ReprocessController} from "./reprocess.js"; @@ -902,6 +911,7 @@ export class BeaconChain implements IBeaconChain { consensusBlockValue: Wei; shouldOverrideBuilder?: boolean; }> { + const fork = this.config.getForkName(slot); const state = await this.regen.getBlockSlotState( parentBlock, slot, @@ -930,7 +940,7 @@ export class BeaconChain implements IBeaconChain { // The hashtree root computed here for debug log will get cached and hence won't introduce additional delays const bodyRoot = produceResult.type === BlockType.Full - ? this.config.getForkTypes(slot).BeaconBlockBody.hashTreeRoot(body) + ? sszTypesFor(fork).BeaconBlockBody.hashTreeRoot(body) : this.config .getPostBellatrixForkTypes(slot) .BlindedBeaconBlockBody.hashTreeRoot(body as BlindedBeaconBlockBody); @@ -948,15 +958,28 @@ export class BeaconChain implements IBeaconChain { body, } as AssembledBlockType; - const {newStateRoot, proposerReward} = computeNewStateRoot(this.metrics, state, block); + const {newStateRoot, proposerReward, postState} = computeNewStateRoot(this.metrics, state, block); block.stateRoot = newStateRoot; const blockRoot = produceResult.type === BlockType.Full - ? this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block) + ? sszTypesFor(fork).BeaconBlock.hashTreeRoot(block) : this.config.getPostBellatrixForkTypes(slot).BlindedBeaconBlock.hashTreeRoot(block as BlindedBeaconBlock); const blockRootHex = toRootHex(blockRoot); - // Track the produced block for consensus broadcast validations, later validation, etc. + if (isForkPostGloas(fork) && produceResult.type === BlockType.Full) { + const gloasResult = produceResult as ProduceFullGloas; + const envelope: gloas.ExecutionPayloadEnvelope = { + payload: gloasResult.executionPayload, + executionRequests: gloasResult.executionRequests, + builderIndex: BUILDER_INDEX_SELF_BUILD, + beaconBlockRoot: blockRoot, + slot, + stateRoot: ZERO_HASH, + }; + const envelopeStateRoot = computeEnvelopeStateRoot(this.metrics, postState as CachedBeaconStateGloas, envelope); + gloasResult.envelopeStateRoot = envelopeStateRoot; + } + this.blockProductionCache.set(blockRootHex, produceResult); this.metrics?.blockProductionCacheSize.set(this.blockProductionCache.size); diff --git a/packages/beacon-node/src/chain/emitter.ts b/packages/beacon-node/src/chain/emitter.ts index 9de32429069f..dec6ea198a96 100644 --- a/packages/beacon-node/src/chain/emitter.ts +++ b/packages/beacon-node/src/chain/emitter.ts @@ -3,7 +3,7 @@ import {StrictEventEmitter} from "strict-event-emitter-types"; import {routes} from "@lodestar/api"; import {CheckpointWithHex} from "@lodestar/fork-choice"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; -import {RootHex, deneb, fulu, phase0} from "@lodestar/types"; +import {DataColumnSidecar, RootHex, deneb, phase0} from "@lodestar/types"; import {PeerIdStr} from "../util/peerId.js"; import {BlockInputSource, IBlockInput} from "./blocks/blockInput/types.js"; @@ -88,7 +88,7 @@ export type IChainEvents = ApiEvents & { [ChainEvent.updateTargetCustodyGroupCount]: (targetGroupCount: number) => void; - [ChainEvent.publishDataColumns]: (sidecars: fulu.DataColumnSidecar[]) => void; + [ChainEvent.publishDataColumns]: (sidecars: DataColumnSidecar[]) => void; [ChainEvent.publishBlobSidecars]: (sidecars: deneb.BlobSidecar[]) => void; diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index f58fcbe48f1a..0d84adc771fd 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -1,14 +1,14 @@ import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; import {getSafeExecutionBlockHash} from "@lodestar/fork-choice"; -import {ForkPostBellatrix, ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; +import {ForkPostBellatrix, ForkSeq, SLOTS_PER_EPOCH, isForkPostBellatrix} from "@lodestar/params"; import { CachedBeaconStateAllForks, CachedBeaconStateExecutions, + CachedBeaconStateGloas, StateHashTreeRootSource, computeEpochAtSlot, computeTimeAtSlot, - isExecutionStateType, } from "@lodestar/state-transition"; import {Slot} from "@lodestar/types"; import {Logger, fromHex, isErrorAborted, sleep} from "@lodestar/utils"; @@ -120,10 +120,10 @@ export class PrepareNextSlotScheduler { RegenCaller.precomputeEpoch ); - if (isExecutionStateType(prepareState)) { + if (isForkPostBellatrix(fork)) { const proposerIndex = prepareState.epochCtx.getBeaconProposer(prepareSlot); const feeRecipient = this.chain.beaconProposerCache.get(proposerIndex); - let updatedPrepareState = prepareState; + let updatedPrepareState = prepareState as CachedBeaconStateExecutions | CachedBeaconStateGloas; let updatedHeadRoot = headRoot; if (feeRecipient) { @@ -146,7 +146,7 @@ export class PrepareNextSlotScheduler { // only transfer cache if epoch transition because that's the state we will use to stateTransition() the 1st block of epoch {dontTransferCache: !isEpochTransition}, RegenCaller.predictProposerHead - )) as CachedBeaconStateExecutions; + )) as CachedBeaconStateExecutions | CachedBeaconStateGloas; updatedHeadRoot = proposerHeadRoot; } diff --git a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts index cf803996b6df..63e2638e9c9f 100644 --- a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts +++ b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts @@ -1,11 +1,14 @@ import { CachedBeaconStateAllForks, + CachedBeaconStateGloas, DataAvailabilityStatus, ExecutionPayloadStatus, + G2_POINT_AT_INFINITY, StateHashTreeRootSource, stateTransition, } from "@lodestar/state-transition"; -import {BeaconBlock, BlindedBeaconBlock, Gwei, Root} from "@lodestar/types"; +import {processExecutionPayloadEnvelope} from "@lodestar/state-transition/block"; +import {BeaconBlock, BlindedBeaconBlock, Gwei, Root, gloas} from "@lodestar/types"; import {ZERO_HASH} from "../../constants/index.js"; import {Metrics} from "../../metrics/index.js"; @@ -18,7 +21,7 @@ export function computeNewStateRoot( metrics: Metrics | null, state: CachedBeaconStateAllForks, block: BeaconBlock | BlindedBeaconBlock -): {newStateRoot: Root; proposerReward: Gwei} { +): {newStateRoot: Root; proposerReward: Gwei; postState: CachedBeaconStateAllForks} { // Set signature to zero to re-use stateTransition() function which requires the SignedBeaconBlock type const blockEmptySig = {message: block, signature: ZERO_HASH}; @@ -51,5 +54,32 @@ export function computeNewStateRoot( const newStateRoot = postState.hashTreeRoot(); hashTreeRootTimer?.(); - return {newStateRoot, proposerReward}; + return {newStateRoot, proposerReward, postState}; +} + +/** + * Compute the state root after processing an execution payload envelope. + * Similar to `computeNewStateRoot` but for payload envelope processing. + * + * The `postBlockState` is mutated in place callers must ensure it is not needed afterward. + */ +export function computeEnvelopeStateRoot( + metrics: Metrics | null, + postBlockState: CachedBeaconStateGloas, + envelope: gloas.ExecutionPayloadEnvelope +): Root { + const signedEnvelope: gloas.SignedExecutionPayloadEnvelope = { + message: envelope, + signature: G2_POINT_AT_INFINITY, + }; + + processExecutionPayloadEnvelope(postBlockState, signedEnvelope, false); + + const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ + source: StateHashTreeRootSource.computeEnvelopeStateRoot, + }); + const stateRoot = postBlockState.hashTreeRoot(); + hashTreeRootTimer?.(); + + return stateRoot; } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 8b13afb5a189..22f30c82c6f7 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -47,7 +47,6 @@ import { electra, fulu, gloas, - ssz, } from "@lodestar/types"; import {Logger, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {ZERO_HASH_HEX} from "../../constants/index.js"; @@ -112,8 +111,15 @@ export type ProduceFullGloas = { executionRequests: electra.ExecutionRequests; blobKzgCommitments: deneb.BlobKzgCommitments; cells: fulu.Cell[][]; - /** Cell proofs for building DataColumnSidecars, 128 proofs per blob */ - cellProofs: Uint8Array[]; + /** Cell proofs for building `DataColumnSidecars`, 128 proofs per blob */ + // cellProofs: Uint8Array[]; // TODO: do we need this? + /** + * Cached envelope state root computed during block production. + * This is the state root after running `processExecutionPayloadEnvelope` on the + * post-block state, avoiding an expensive state clone when the validator API + * later constructs the `ExecutionPayloadEnvelope`. + */ + envelopeStateRoot: Root; }; export type ProduceFullFulu = { type: BlockType.Full; @@ -197,7 +203,6 @@ export async function produceBlockBody( this.logger.verbose("Producing beacon block body", logMeta); if (isForkPostGloas(fork)) { - // Post-Gloas block production: get execution payload from EL, create self-build bid const gloasState = currentState as CachedBeaconStateGloas; const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; @@ -205,17 +210,17 @@ export async function produceBlockBody( const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer(); - this.logger.verbose("Preparing Gloas execution payload from engine", { + this.logger.verbose("Preparing execution payload from engine", { slot: blockSlot, parentBlockRoot: toRootHex(parentBlockRoot), feeRecipient, }); - // Get execution payload from EL (similar to pre-Gloas, but we don't include it in block body) - const prepareRes = await prepareExecutionPayloadGloas( + // Get execution payload from EL + const prepareRes = await prepareExecutionPayload( this, this.logger, - fork as ForkPostGloas, + fork, parentBlockRoot, safeBlockHash, finalizedBlockHash ?? ZERO_HASH_HEX, @@ -230,7 +235,7 @@ export async function produceBlockBody( await sleep(PAYLOAD_GENERATION_TIME_MS); } - this.logger.verbose("Fetching Gloas execution payload from engine", {slot: blockSlot, payloadId}); + this.logger.verbose("Fetching execution payload from engine", {slot: blockSlot, payloadId}); const payloadRes = await this.executionEngine.getPayload(fork, payloadId); endExecutionPayload?.({step: BlockProductionStep.executionPayload}); @@ -246,15 +251,12 @@ export async function produceBlockBody( throw Error(`Missing executionRequests response from getPayload at fork=${fork}`); } - // Compute cells for PeerDAS const cells = blobsBundle.blobs.map((blob) => kzg.computeCells(blob)); if (this.opts.sanityCheckExecutionEngineBlobs) { await validateCellsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, cells); } // Create self-build execution payload bid - // For self-builds, builder_index = BUILDER_INDEX_SELF_BUILD (UINT64_MAX) - // and value/executionPayment are 0 const bid: gloas.ExecutionPayloadBid = { parentBlockHash: gloasState.latestBlockHash, parentBlockRoot: parentBlockRoot, @@ -268,35 +270,28 @@ export async function produceBlockBody( executionPayment: 0, blobKzgCommitments: blobsBundle.commitments, }; - - // For self-builds, signature is G2_POINT_AT_INFINITY const signedBid: gloas.SignedExecutionPayloadBid = { message: bid, signature: G2_POINT_AT_INFINITY, }; - // Get common block body and add Gloas-specific fields const commonBlockBody = await commonBlockBodyPromise; const gloasBody = Object.assign({}, commonBlockBody) as gloas.BeaconBlockBody; gloasBody.signedExecutionPayloadBid = signedBid; - // TODO GLOAS: Get payload attestations from pool for previous slot - // For now, set empty payload attestations gloasBody.payloadAttestations = []; - blockBody = gloasBody as AssembledBodyType; - // Store execution payload data in produceResult for envelope creation - (produceResult as ProduceFullGloas).executionPayload = executionPayload as ExecutionPayload; - (produceResult as ProduceFullGloas).executionRequests = executionRequests; - (produceResult as ProduceFullGloas).blobKzgCommitments = blobsBundle.commitments; - (produceResult as ProduceFullGloas).cells = cells; - // Store cell proofs for building DataColumnSidecars during envelope publishing - (produceResult as ProduceFullGloas).cellProofs = blobsBundle.proofs; + // Store execution payload data in produceResult for envelope creation later + const gloasResult = produceResult as ProduceFullGloas; + gloasResult.executionPayload = executionPayload as ExecutionPayload; + gloasResult.executionRequests = executionRequests; + gloasResult.blobKzgCommitments = blobsBundle.commitments; + gloasResult.cells = cells; const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime); this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime); - this.logger.verbose("Produced Gloas block with self-build bid", { + this.logger.verbose("Produced block with self-build bid", { slot: blockSlot, executionPayloadValue, prepType, @@ -605,10 +600,12 @@ export async function prepareExecutionPayload( parentBlockRoot: Root, safeBlockHash: RootHex, finalizedBlockHash: RootHex, - state: CachedBeaconStateExecutions, + state: CachedBeaconStateExecutions | CachedBeaconStateGloas, suggestedFeeRecipient: string ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { - const parentHash = state.latestExecutionPayloadHeader.blockHash; + const parentHash = isForkPostGloas(fork) + ? (state as CachedBeaconStateGloas).latestBlockHash + : (state as CachedBeaconStateExecutions).latestExecutionPayloadHeader.blockHash; const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime); const prevRandao = getRandaoMix(state, state.epochCtx.epoch); @@ -667,73 +664,6 @@ export async function prepareExecutionPayload( return {payloadId, prepType}; } -/** - * Produce ExecutionPayload for Gloas. - * Similar to prepareExecutionPayload but uses latestBlockHash instead of latestExecutionPayloadHeader. - */ -export async function prepareExecutionPayloadGloas( - chain: { - executionEngine: IExecutionEngine; - config: ChainForkConfig; - }, - logger: Logger, - fork: ForkPostGloas, - parentBlockRoot: Root, - safeBlockHash: RootHex, - finalizedBlockHash: RootHex, - state: CachedBeaconStateGloas, - suggestedFeeRecipient: string -): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { - // Gloas uses latestBlockHash instead of latestExecutionPayloadHeader.blockHash - const parentHash = state.latestBlockHash; - const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime); - const prevRandao = getRandaoMix(state, state.epochCtx.epoch); - - const payloadIdCached = chain.executionEngine.payloadIdCache.get({ - headBlockHash: toRootHex(parentHash), - finalizedBlockHash, - timestamp: numToQuantity(timestamp), - prevRandao: toHex(prevRandao), - suggestedFeeRecipient, - }); - - let payloadId: PayloadId | null; - let prepType: PayloadPreparationType; - - if (payloadIdCached) { - payloadId = payloadIdCached; - prepType = PayloadPreparationType.Cached; - } else { - if (chain.executionEngine.payloadIdCache.hasPayload({timestamp: numToQuantity(timestamp)})) { - prepType = PayloadPreparationType.Reorged; - } else { - prepType = PayloadPreparationType.Fresh; - } - - const attributes: PayloadAttributes = preparePayloadAttributesGloas(fork, chain, { - prepareState: state, - prepareSlot: state.slot, - parentBlockRoot, - feeRecipient: suggestedFeeRecipient, - }); - - payloadId = await chain.executionEngine.notifyForkchoiceUpdate( - fork, - toRootHex(parentHash), - safeBlockHash, - finalizedBlockHash, - attributes - ); - logger.verbose("Prepared Gloas payload id from execution engine", {payloadId}); - } - - if (payloadId === null) { - throw Error("notifyForkchoiceUpdate returned payloadId null"); - } - - return {payloadId, prepType}; -} - async function prepareExecutionPayloadHeader( chain: { executionBuilder?: IExecutionBuilder; @@ -766,9 +696,16 @@ export function getPayloadAttributesForSSE( prepareSlot, parentBlockRoot, feeRecipient, - }: {prepareState: CachedBeaconStateExecutions; prepareSlot: Slot; parentBlockRoot: Root; feeRecipient: string} + }: { + prepareState: CachedBeaconStateExecutions | CachedBeaconStateGloas; + prepareSlot: Slot; + parentBlockRoot: Root; + feeRecipient: string; + } ): SSEPayloadAttributes { - const parentHash = prepareState.latestExecutionPayloadHeader.blockHash; + const parentHash = isForkPostGloas(fork) + ? (prepareState as CachedBeaconStateGloas).latestBlockHash + : (prepareState as CachedBeaconStateExecutions).latestExecutionPayloadHeader.blockHash; const payloadAttributes = preparePayloadAttributes(fork, chain, { prepareState, prepareSlot, @@ -778,7 +715,10 @@ export function getPayloadAttributesForSSE( const ssePayloadAttributes: SSEPayloadAttributes = { proposerIndex: prepareState.epochCtx.getBeaconProposer(prepareSlot), proposalSlot: prepareSlot, - parentBlockNumber: prepareState.latestExecutionPayloadHeader.blockNumber, + // TODO GLOAS: latestExecutionPayloadHeader does not exist on Gloas state, parentBlockNumber needs different source + parentBlockNumber: isForkPostGloas(fork) + ? 0 + : (prepareState as CachedBeaconStateExecutions).latestExecutionPayloadHeader.blockNumber, parentBlockRoot, parentBlockHash: parentHash, payloadAttributes, @@ -797,7 +737,7 @@ function preparePayloadAttributes( parentBlockRoot, feeRecipient, }: { - prepareState: CachedBeaconStateExecutions; + prepareState: CachedBeaconStateExecutions | CachedBeaconStateGloas; prepareSlot: Slot; parentBlockRoot: Root; feeRecipient: string; @@ -826,44 +766,6 @@ function preparePayloadAttributes( return payloadAttributes; } -/** - * Prepare payload attributes for Gloas forks. - * Similar to preparePayloadAttributes but uses Gloas state structure. - */ -function preparePayloadAttributesGloas( - fork: ForkPostGloas, - chain: { - config: ChainForkConfig; - }, - { - prepareState, - prepareSlot, - parentBlockRoot, - feeRecipient, - }: { - prepareState: CachedBeaconStateGloas; - prepareSlot: Slot; - parentBlockRoot: Root; - feeRecipient: string; - } -): PayloadAttributes { - const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime); - const prevRandao = getRandaoMix(prepareState, prepareState.epochCtx.epoch); - - // Get withdrawals using the same withdrawal logic as electra - const {expectedWithdrawals} = getExpectedWithdrawals(ForkSeq[fork], prepareState); - - const payloadAttributes: PayloadAttributes = { - timestamp, - prevRandao, - suggestedFeeRecipient: feeRecipient, - withdrawals: expectedWithdrawals, - parentBeaconBlockRoot: parentBlockRoot, - }; - - return payloadAttributes; -} - export async function produceCommonBlockBody( this: BeaconChain, blockType: T, diff --git a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts index 82855a6300c3..e10ce40f7a9d 100644 --- a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts +++ b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts @@ -10,7 +10,7 @@ import { getBlockHeaderProposerSignatureSetByHeaderSlot, getBlockHeaderProposerSignatureSetByParentStateSlot, } from "@lodestar/state-transition"; -import {Root, Slot, SubnetID, fulu, ssz} from "@lodestar/types"; +import {DataColumnSidecar, Root, Slot, SubnetID, fulu, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex, verifyMerkleBranch} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {kzg} from "../../util/kzg.js"; @@ -457,9 +457,6 @@ export async function validateBlockDataColumnSidecars( * SPEC FUNCTION * https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.4/specs/fulu/p2p-interface.md#compute_subnet_for_data_column_sidecar */ -export function computeSubnetForDataColumnSidecar( - config: ChainConfig, - columnSidecar: fulu.DataColumnSidecar -): SubnetID { +export function computeSubnetForDataColumnSidecar(config: ChainConfig, columnSidecar: DataColumnSidecar): SubnetID { return columnSidecar.index % config.DATA_COLUMN_SIDECAR_SUBNET_COUNT; } diff --git a/packages/beacon-node/src/network/gossip/interface.ts b/packages/beacon-node/src/network/gossip/interface.ts index 54904cd28e26..f7544b91f67b 100644 --- a/packages/beacon-node/src/network/gossip/interface.ts +++ b/packages/beacon-node/src/network/gossip/interface.ts @@ -4,6 +4,7 @@ import {PeerIdStr} from "@chainsafe/libp2p-gossipsub/types"; import {BeaconConfig, ForkBoundary} from "@lodestar/config"; import { AttesterSlashing, + DataColumnSidecar, LightClientFinalityUpdate, LightClientOptimisticUpdate, SignedAggregateAndProof, @@ -14,7 +15,6 @@ import { altair, capella, deneb, - fulu, gloas, phase0, } from "@lodestar/types"; @@ -98,7 +98,7 @@ export type GossipTypeMap = { [GossipType.blob_sidecar]: deneb.BlobSidecar; [GossipType.beacon_aggregate_and_proof]: SignedAggregateAndProof; [GossipType.beacon_attestation]: SingleAttestation; - [GossipType.data_column_sidecar]: fulu.DataColumnSidecar; + [GossipType.data_column_sidecar]: DataColumnSidecar; [GossipType.voluntary_exit]: phase0.SignedVoluntaryExit; [GossipType.proposer_slashing]: phase0.ProposerSlashing; [GossipType.attester_slashing]: AttesterSlashing; @@ -117,7 +117,7 @@ export type GossipFnByType = { [GossipType.blob_sidecar]: (blobSidecar: deneb.BlobSidecar) => Promise | void; [GossipType.beacon_aggregate_and_proof]: (aggregateAndProof: SignedAggregateAndProof) => Promise | void; [GossipType.beacon_attestation]: (attestation: SingleAttestation) => Promise | void; - [GossipType.data_column_sidecar]: (dataColumnSidecar: fulu.DataColumnSidecar) => Promise | void; + [GossipType.data_column_sidecar]: (dataColumnSidecar: DataColumnSidecar) => Promise | void; [GossipType.voluntary_exit]: (voluntaryExit: phase0.SignedVoluntaryExit) => Promise | void; [GossipType.proposer_slashing]: (proposerSlashing: phase0.ProposerSlashing) => Promise | void; [GossipType.attester_slashing]: (attesterSlashing: AttesterSlashing) => Promise | void; diff --git a/packages/beacon-node/src/network/gossip/topic.ts b/packages/beacon-node/src/network/gossip/topic.ts index e81087288ada..e01343d6bbf5 100644 --- a/packages/beacon-node/src/network/gossip/topic.ts +++ b/packages/beacon-node/src/network/gossip/topic.ts @@ -6,6 +6,7 @@ import { SYNC_COMMITTEE_SUBNET_COUNT, isForkPostAltair, isForkPostElectra, + isForkPostFulu, } from "@lodestar/params"; import {Attestation, SingleAttestation, ssz, sszTypesFor} from "@lodestar/types"; import {GossipAction, GossipActionError, GossipErrorCode} from "../../chain/errors/gossipValidation.js"; @@ -92,7 +93,7 @@ export function getGossipSSZType(topic: GossipTopic) { case GossipType.blob_sidecar: return ssz.deneb.BlobSidecar; case GossipType.data_column_sidecar: - return ssz.fulu.DataColumnSidecar; + return isForkPostFulu(fork) ? sszTypesFor(fork).DataColumnSidecar : ssz.fulu.DataColumnSidecar; case GossipType.beacon_aggregate_and_proof: return sszTypesFor(fork).SignedAggregateAndProof; case GossipType.beacon_attestation: diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index 33e3de1669f8..998948644e5f 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -19,6 +19,7 @@ import type {Datastore} from "interface-datastore"; import {Libp2p as ILibp2p} from "libp2p"; import { AttesterSlashing, + DataColumnSidecar, LightClientFinalityUpdate, LightClientOptimisticUpdate, SignedAggregateAndProof, @@ -87,7 +88,7 @@ export interface INetwork extends INetworkCorePublic { publishBlobSidecar(blobSidecar: deneb.BlobSidecar): Promise; publishBeaconAggregateAndProof(aggregateAndProof: SignedAggregateAndProof): Promise; publishBeaconAttestation(attestation: SingleAttestation, subnet: SubnetID): Promise; - publishDataColumnSidecar(dataColumnSideCar: fulu.DataColumnSidecar): Promise; + publishDataColumnSidecar(dataColumnSideCar: DataColumnSidecar): Promise; publishVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise; publishBlsToExecutionChange(blsToExecutionChange: capella.SignedBLSToExecutionChange): Promise; publishProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise; diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index db44429886f2..3e5243efb270 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -10,6 +10,7 @@ import {ResponseIncoming} from "@lodestar/reqresp"; import {computeEpochAtSlot} from "@lodestar/state-transition"; import { AttesterSlashing, + DataColumnSidecar, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, @@ -34,7 +35,7 @@ import {computeSubnetForDataColumnSidecar} from "../chain/validation/dataColumnS import {IBeaconDb} from "../db/interface.js"; import {Metrics, RegistryMetricCreator} from "../metrics/index.js"; import {IClock} from "../util/clock.js"; -import {CustodyConfig} from "../util/dataColumns.js"; +import {CustodyConfig, isGloasDataColumnSidecar} from "../util/dataColumns.js"; import {PeerIdStr, peerIdToString} from "../util/peerId.js"; import {promiseAllMaybeAsync} from "../util/promises.js"; import {BeaconBlocksByRootRequest, BlobSidecarsByRootRequest, DataColumnSidecarsByRootRequest} from "../util/types.js"; @@ -355,8 +356,11 @@ export class Network implements INetwork { }); } - async publishDataColumnSidecar(dataColumnSidecar: fulu.DataColumnSidecar): Promise { - const epoch = computeEpochAtSlot(dataColumnSidecar.signedBlockHeader.message.slot); + async publishDataColumnSidecar(dataColumnSidecar: DataColumnSidecar): Promise { + const slot = isGloasDataColumnSidecar(dataColumnSidecar) + ? dataColumnSidecar.slot + : dataColumnSidecar.signedBlockHeader.message.slot; + const epoch = computeEpochAtSlot(slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); const subnet = computeSubnetForDataColumnSidecar(this.config, dataColumnSidecar); @@ -777,7 +781,7 @@ export class Network implements INetwork { this.core.setTargetGroupCount(count); }; - private onPublishDataColumns = (sidecars: fulu.DataColumnSidecar[]): Promise => { + private onPublishDataColumns = (sidecars: DataColumnSidecar[]): Promise => { return promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishDataColumnSidecar(sidecar))); }; diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 996a26148785..8d503bb1b6bd 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -548,7 +548,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand seenTimestampSec, }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; - const dataColumnSidecar = sszDeserialize(topic, serializedData); + // TODO GLOAS: handle gloas.DataColumnSidecar + const dataColumnSidecar = sszDeserialize(topic, serializedData) as fulu.DataColumnSidecar; const dataColumnSlot = dataColumnSidecar.signedBlockHeader.message.slot; const index = dataColumnSidecar.index; diff --git a/packages/beacon-node/src/util/dataColumns.ts b/packages/beacon-node/src/util/dataColumns.ts index 0b53c72e4155..c486755119ea 100644 --- a/packages/beacon-node/src/util/dataColumns.ts +++ b/packages/beacon-node/src/util/dataColumns.ts @@ -15,9 +15,12 @@ import { BeaconBlockBody, ColumnIndex, CustodyIndex, + DataColumnSidecar, + Root, SSZTypesFor, SignedBeaconBlock, SignedBeaconBlockHeader, + Slot, deneb, fulu, gloas, @@ -277,6 +280,11 @@ export function getBlobKzgCommitments( return (signedBlock.message.body as BeaconBlockBody).blobKzgCommitments; } +/** Type guard for gloas.DataColumnSidecar */ +export function isGloasDataColumnSidecar(sidecar: DataColumnSidecar): sidecar is gloas.DataColumnSidecar { + return (sidecar as gloas.DataColumnSidecar).beaconBlockRoot !== undefined; +} + /** * Given a signed block header and the commitments, inclusion proof, cells/proofs associated with * each blob in the block, assemble the sidecars which can be distributed to peers. @@ -361,26 +369,36 @@ export function getDataColumnSidecarsFromColumnSidecar( /** * For GLOAS self-builds: build DataColumnSidecars from the envelope data. - * In GLOAS, blobKzgCommitments are in the ExecutionPayloadEnvelope, not the BeaconBlockBody. - * The inclusion proof is computed against the envelope's blobKzgCommitments field. + * In GLOAS, DataColumnSidecar has a simplified structure with `slot` and `beaconBlockRoot` + * instead of `signedBlockHeader`, `kzgCommitments`, and `kzgCommitmentsInclusionProof`. */ export function getDataColumnSidecarsForGloas( - signedBlockHeader: SignedBeaconBlockHeader, - blobKzgCommitments: deneb.BlobKzgCommitments, + slot: Slot, + beaconBlockRoot: Root, cellsAndKzgProofs: {cells: Uint8Array[]; proofs: Uint8Array[]}[] -): fulu.DataColumnSidecars { +): gloas.DataColumnSidecars { // No need to create data column sidecars if there are no blobs - if (blobKzgCommitments.length === 0) { + if (cellsAndKzgProofs.length === 0) { return []; } - // For GLOAS, the inclusion proof is for the envelope's blobKzgCommitments field - // The envelope structure has a different gindex than the block body - // TODO GLOAS: Compute proper inclusion proof for envelope's blobKzgCommitments - // For now, use an empty proof since the envelope already contains the commitments explicitly - const kzgCommitmentsInclusionProof = new Array(17).fill(new Uint8Array(32)) as fulu.KzgCommitmentsInclusionProof; - - return getDataColumnSidecars(signedBlockHeader, blobKzgCommitments, kzgCommitmentsInclusionProof, cellsAndKzgProofs); + const sidecars: gloas.DataColumnSidecars = []; + for (let columnIndex = 0; columnIndex < NUMBER_OF_COLUMNS; columnIndex++) { + const column: Uint8Array[] = []; + const kzgProofs: Uint8Array[] = []; + for (const {cells, proofs} of cellsAndKzgProofs) { + column.push(cells[columnIndex]); + kzgProofs.push(proofs[columnIndex]); + } + sidecars.push({ + index: columnIndex, + column, + kzgProofs, + slot, + beaconBlockRoot, + }); + } + return sidecars; } /** diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 9e1f006cf832..9db9297db52e 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -76,6 +76,7 @@ export enum StateHashTreeRootSource { prepareNextEpoch = "prepare_next_epoch", regenState = "regen_state", computeNewStateRoot = "compute_new_state_root", + computeEnvelopeStateRoot = "compute_envelope_state_root", } /** diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 5591833f7157..223269815f6e 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -1,4 +1,12 @@ -import {ForkAll, ForkName, ForkPostAltair, ForkPostBellatrix, ForkPostDeneb, ForkPostElectra} from "@lodestar/params"; +import { + ForkAll, + ForkName, + ForkPostAltair, + ForkPostBellatrix, + ForkPostDeneb, + ForkPostElectra, + ForkPostFulu, +} from "@lodestar/params"; import {ts as altair} from "./altair/index.js"; import {ts as bellatrix} from "./bellatrix/index.js"; import {ts as capella} from "./capella/index.js"; @@ -273,6 +281,8 @@ type TypesByFork = { AggregateAndProof: electra.AggregateAndProof; SignedAggregateAndProof: electra.SignedAggregateAndProof; ExecutionRequests: electra.ExecutionRequests; + DataColumnSidecar: fulu.DataColumnSidecar; + DataColumnSidecars: fulu.DataColumnSidecars; }; [ForkName.gloas]: { BeaconBlockHeader: phase0.BeaconBlockHeader; @@ -311,6 +321,8 @@ type TypesByFork = { AggregateAndProof: electra.AggregateAndProof; SignedAggregateAndProof: electra.SignedAggregateAndProof; ExecutionRequests: electra.ExecutionRequests; + DataColumnSidecar: gloas.DataColumnSidecar; + DataColumnSidecars: gloas.DataColumnSidecars; }; }; @@ -345,6 +357,9 @@ export type ExecutionPayloadAndBlobsBundle = TypesByFork[F]["BlobsBundle"]; +export type DataColumnSidecar = TypesByFork[F]["DataColumnSidecar"]; +export type DataColumnSidecars = TypesByFork[F]["DataColumnSidecars"]; + export type LightClientHeader = TypesByFork[F]["LightClientHeader"]; export type LightClientBootstrap = TypesByFork[F]["LightClientBootstrap"]; export type LightClientUpdate = TypesByFork[F]["LightClientUpdate"]; diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index e967eb22c278..9038b850faea 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -1,6 +1,6 @@ import {ApiClient, routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; -import {isForkPostGloas} from "@lodestar/params"; +import {BUILDER_INDEX_SELF_BUILD, isForkPostGloas} from "@lodestar/params"; import { BLSPubkey, BLSSignature, @@ -217,17 +217,11 @@ export class BlockProposingService { this.logger.debug("Published Gloas beacon block", debugLogCtx); // Step 3: Get the execution payload envelope - // For self-builds, the builder index is the proposer's validator index - const validatorIndex = this.validatorStore.getValidatorIndex(pubkeyHex); - if (validatorIndex === undefined) { - throw new Error(`Validator index not found for ${pubkeyHex}`); - } - const beaconBlockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block); const envelopeRes = await this.api.validator.getExecutionPayloadEnvelope({ slot, beaconBlockRoot, - builderIndex: validatorIndex, + builderIndex: BUILDER_INDEX_SELF_BUILD, }); const envelope = envelopeRes.value(); From c57ded85489b07bca556e59734dbc281a55dc011 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 13 Feb 2026 22:02:21 +0000 Subject: [PATCH 11/34] Update beacon-api spec to v5.0.0-alpha.0 --- .../api/test/unit/beacon/oapiSpec.test.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/api/test/unit/beacon/oapiSpec.test.ts b/packages/api/test/unit/beacon/oapiSpec.test.ts index f435abfb3fec..590942f8f992 100644 --- a/packages/api/test/unit/beacon/oapiSpec.test.ts +++ b/packages/api/test/unit/beacon/oapiSpec.test.ts @@ -20,14 +20,14 @@ import {testData as validatorTestData} from "./testData/validator.js"; // Solutions: https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-js-when-using-es6-modules const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const version = "v4.0.0-alpha.1"; +const version = "v5.0.0-alpha.0"; const openApiFile: OpenApiFile = { url: `https://github.com/ethereum/beacon-APIs/releases/download/${version}/beacon-node-oapi.json`, filepath: path.join(__dirname, "../../../oapi-schemas/beacon-node-oapi.json"), version: RegExp(version), }; -const config = createChainForkConfig({...defaultChainConfig, ELECTRA_FORK_EPOCH: 0}); +const config = createChainForkConfig({...defaultChainConfig, FULU_FORK_EPOCH: 0, GLOAS_FORK_EPOCH: 0}); const definitions = { ...routes.beacon.getDefinitions(config), @@ -55,6 +55,14 @@ const ignoredOperations = [ /* missing route */ "getDepositSnapshot", // Won't fix for now, see https://github.com/ChainSafe/lodestar/issues/5697 "getNextWithdrawals", // https://github.com/ChainSafe/lodestar/issues/5696 + // TODO GLOAS: required by v5.0.0-alpha.0 + "publishExecutionPayloadBid", + "getSignedExecutionPayloadEnvelope", + "getPoolPayloadAttestations", + "submitPayloadAttestationMessages", + "getPtcDuties", + "producePayloadAttestationData", + "getExecutionPayloadBid", ]; const ignoredProperties: Record = { @@ -68,7 +76,12 @@ const ignoredProperties: Record = { const openApiJson = await fetchOpenApiSpec(openApiFile); runTestCheckAgainstSpec(openApiJson, definitions, testDatas, ignoredOperations, ignoredProperties); -const ignoredTopics: string[] = []; +const ignoredTopics: string[] = [ + // TODO GLOAS: required by v5.0.0-alpha.0 + "execution_payload_available", + "execution_payload_bid", + "payload_attestation_message", +]; // eventstream types are defined as comments in the description of "examples". // The function runTestCheckAgainstSpec() can't handle those, so the custom code before: From 65be0b4113c79f37d68e0c4b6e63d4cd53f86cbf Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 10:18:41 +0000 Subject: [PATCH 12/34] Review publishExecutionPayloadEnvelope --- .../src/api/impl/beacon/blocks/index.ts | 48 +++++++++++++++---- .../chain/produceBlock/produceBlockBody.ts | 6 +-- .../src/metrics/metrics/lodestar.ts | 9 ++++ .../src/network/processor/gossipHandlers.ts | 5 ++ 4 files changed, 55 insertions(+), 13 deletions(-) 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 8332617bd1f4..2e00ab7053f1 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -656,23 +656,26 @@ export function getBeaconBlockApi({ const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; let dataColumnSidecars: gloas.DataColumnSidecars = []; - // For self-builds, retrieve cached production data and build DataColumnSidecars if (isSelfBuild) { + // For self-builds, construct and publish data column sidecars from cached block production data const cachedResult = chain.blockProductionCache.get(blockRootHex) as ProduceFullGloas | undefined; - if (cachedResult?.cells && cachedResult.blobKzgCommitments.length > 0) { - // TODO GLOAS: cell proofs are not currently cached, need to store them during block production - // Build cellsAndProofs from the cached cells - const cellsAndProofs = cachedResult.cells.map((rowCells) => ({ + if (cachedResult?.cells && cachedResult.blobsBundle.commitments.length > 0) { + const cellsAndProofs = cachedResult.cells.map((rowCells, rowIndex) => ({ cells: rowCells, - proofs: [] as Uint8Array[], + proofs: cachedResult.blobsBundle.proofs.slice( + rowIndex * NUMBER_OF_COLUMNS, + (rowIndex + 1) * NUMBER_OF_COLUMNS + ), })); dataColumnSidecars = getDataColumnSidecarsForGloas(slot, envelope.beaconBlockRoot, cellsAndProofs); } + } else { + // TODO GLOAS: will this api be used by builders or only for self-building? } // TODO GLOAS: Verify execution payload envelope signature - // For self-builds, the proposer signs with their own key (NOT G2_POINT_AT_INFINITY — that's only for the bid) + // For self-builds, the proposer signs with their own validator key // For external builders, verify using the builder's registered pubkey // Use verify_execution_payload_envelope_signature(state, signed_envelope) @@ -682,6 +685,9 @@ export function getBeaconBlockApi({ // TODO GLOAS: Update fork choice with the execution payload // Call on_execution_payload(store, signed_envelope) to update fork choice state + // TODO GLOAS: Add envelope and data columns to block input via seenBlockInputCache + // and trigger block import (Gloas block import requires both beacon block and envelope) + const valLogMeta = { slot, blockRoot: blockRootHex, @@ -690,6 +696,15 @@ export function getBeaconBlockApi({ dataColumns: dataColumnSidecars.length, }; + // Simple implementation of a pending envelope queue. If envelope is a bit early, hold it. + const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now(); + if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) { + await sleep(msToBlockSlot); + } + + const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime); + metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.api}, delaySec); + chain.logger.info("Publishing execution payload envelope", valLogMeta); // Publish envelope and data columns @@ -714,15 +729,30 @@ export function getBeaconBlockApi({ } } if (columnsPublishedWithZeroPeers > 0) { - chain.logger.warn("Published data columns to 0 peers for GLOAS envelope", { + chain.logger.warn("Published data columns to 0 peers, increased risk of reorg", { slot, blockRoot: blockRootHex, columns: columnsPublishedWithZeroPeers, }); } + + metrics?.dataColumns.bySource.inc({source: BlockInputSource.api}, dataColumnSidecars.length); + + if (chain.emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) { + // Gloas DataColumnSidecar does not have kzgCommitments, get from cached blobsBundle + const cachedResult = chain.blockProductionCache.get(blockRootHex) as ProduceFullGloas | undefined; + const kzgCommitments = cachedResult?.blobsBundle.commitments.map(toHex) ?? []; + for (const dataColumnSidecar of dataColumnSidecars) { + chain.emitter.emit(routes.events.EventType.dataColumnSidecar, { + blockRoot: blockRootHex, + slot, + index: dataColumnSidecar.index, + kzgCommitments, + }); + } + } } - const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime); chain.logger.info("Published execution payload envelope", { ...valLogMeta, delaySec, diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 22f30c82c6f7..28f86e3e07da 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -109,10 +109,8 @@ export type ProduceFullGloas = { fork: ForkPostGloas; executionPayload: ExecutionPayload; executionRequests: electra.ExecutionRequests; - blobKzgCommitments: deneb.BlobKzgCommitments; + blobsBundle: BlobsBundle; cells: fulu.Cell[][]; - /** Cell proofs for building `DataColumnSidecars`, 128 proofs per blob */ - // cellProofs: Uint8Array[]; // TODO: do we need this? /** * Cached envelope state root computed during block production. * This is the state root after running `processExecutionPayloadEnvelope` on the @@ -286,7 +284,7 @@ export async function produceBlockBody( const gloasResult = produceResult as ProduceFullGloas; gloasResult.executionPayload = executionPayload as ExecutionPayload; gloasResult.executionRequests = executionRequests; - gloasResult.blobKzgCommitments = blobsBundle.commitments; + gloasResult.blobsBundle = blobsBundle; gloasResult.cells = cells; const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime); diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index b4c3eab0efef..54bb57984d8e 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -827,6 +827,15 @@ export function createLodestarMetrics( help: "Total number of blobs retrieved from execution engine and published to gossip", }), }, + // Gossip execution payload envelope + gossipExecutionPayloadEnvelope: { + elapsedTimeTillReceived: register.histogram<{source: OpSource}>({ + name: "lodestar_gossip_execution_payload_envelope_elapsed_time_till_received", + help: "Time elapsed between slot time and the time execution payload envelope received", + labelNames: ["source"], + buckets: [0.5, 1, 2, 4, 6, 12], + }), + }, recoverDataColumnSidecars: { recoverTime: register.histogram({ name: "lodestar_recover_data_column_sidecar_recover_time_seconds", diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 8d503bb1b6bd..e196c90cc8ea 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -822,11 +822,16 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand [GossipType.execution_payload]: async ({ gossipData, topic, + seenTimestampSec, }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const executionPayloadEnvelope = sszDeserialize(topic, serializedData); await validateGossipExecutionPayloadEnvelope(chain, executionPayloadEnvelope); + const slot = executionPayloadEnvelope.message.slot; + const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime); + metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec); + // TODO GLOAS: Handle valid envelope. Need an import flow that calls `processExecutionPayloadEnvelope` and fork choice }, [GossipType.payload_attestation_message]: async ({ From 3b28c1f3619533a4915b842e17d4248a73958854 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 10:37:38 +0000 Subject: [PATCH 13/34] Add todo for data column sidecar event --- packages/beacon-node/src/api/impl/beacon/blocks/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2e00ab7053f1..888ec0251c3d 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -739,7 +739,7 @@ export function getBeaconBlockApi({ metrics?.dataColumns.bySource.inc({source: BlockInputSource.api}, dataColumnSidecars.length); if (chain.emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) { - // Gloas DataColumnSidecar does not have kzgCommitments, get from cached blobsBundle + // TODO GLOAS: revisit this, we likely don't wanna emit KZG commitments anymore const cachedResult = chain.blockProductionCache.get(blockRootHex) as ProduceFullGloas | undefined; const kzgCommitments = cachedResult?.blobsBundle.commitments.map(toHex) ?? []; for (const dataColumnSidecar of dataColumnSidecars) { From af1faa9e8bc4a2c3e33404f0d1cad1269094f0bb Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 10:48:36 +0000 Subject: [PATCH 14/34] Review getExecutionPayloadEnvelope --- packages/beacon-node/src/api/impl/validator/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 8ade8edf7f54..0898ced0de5f 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1622,6 +1622,14 @@ export function getValidatorApi( stateRoot: envelopeStateRoot, }; + logger.info("Produced execution payload envelope", { + slot, + blockRoot: blockRootHex, + builderIndex, + transactions: executionPayload.transactions.length, + blockHash: toRootHex(executionPayload.blockHash), + }); + return { data: envelope, meta: { From 1d48e2962dbf81b064ae2331985ffe1e753c5493 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 10:58:48 +0000 Subject: [PATCH 15/34] Fix parentBlockNumber --- .../chain/produceBlock/computeNewStateRoot.ts | 2 +- .../chain/produceBlock/produceBlockBody.ts | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts index 63e2638e9c9f..5bdbe879105a 100644 --- a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts +++ b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts @@ -61,7 +61,7 @@ export function computeNewStateRoot( * Compute the state root after processing an execution payload envelope. * Similar to `computeNewStateRoot` but for payload envelope processing. * - * The `postBlockState` is mutated in place callers must ensure it is not needed afterward. + * The `postBlockState` is mutated in place, callers must ensure it is not needed afterward. */ export function computeEnvelopeStateRoot( metrics: Metrics | null, diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 28f86e3e07da..07f440de7007 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -1,5 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; -import {ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; +import {IForkChoice, ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; import { BUILDER_INDEX_SELF_BUILD, ForkName, @@ -688,6 +688,7 @@ export function getPayloadAttributesForSSE( fork: ForkPostBellatrix, chain: { config: ChainForkConfig; + forkChoice: IForkChoice; }, { prepareState, @@ -710,13 +711,23 @@ export function getPayloadAttributesForSSE( parentBlockRoot, feeRecipient, }); + + let parentBlockNumber: number; + if (isForkPostGloas(fork)) { + // TODO GLOAS: revisit this after fork choice changes are merged + const parentBlock = chain.forkChoice.getBlock(parentBlockRoot); + if (parentBlock?.executionPayloadBlockHash == null) { + throw Error(`Parent block not found in fork choice root=${toRootHex(parentBlockRoot)}`); + } + parentBlockNumber = parentBlock.executionPayloadNumber; + } else { + parentBlockNumber = (prepareState as CachedBeaconStateExecutions).latestExecutionPayloadHeader.blockNumber; + } + const ssePayloadAttributes: SSEPayloadAttributes = { proposerIndex: prepareState.epochCtx.getBeaconProposer(prepareSlot), proposalSlot: prepareSlot, - // TODO GLOAS: latestExecutionPayloadHeader does not exist on Gloas state, parentBlockNumber needs different source - parentBlockNumber: isForkPostGloas(fork) - ? 0 - : (prepareState as CachedBeaconStateExecutions).latestExecutionPayloadHeader.blockNumber, + parentBlockNumber, parentBlockRoot, parentBlockHash: parentHash, payloadAttributes, From b847fc4aa902b76810b4d5262f7de3ad0b4ecc14 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 11:06:28 +0000 Subject: [PATCH 16/34] There is no slashing for signing two envelopes --- packages/validator/src/services/block.ts | 2 +- .../validator/src/services/validatorStore.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index 9038b850faea..a28a28356751 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -228,7 +228,7 @@ export class BlockProposingService { this.logger.debug("Retrieved execution payload envelope", debugLogCtx); // Step 4: Sign and publish the envelope - const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(pubkey, envelope, slot); + const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(pubkey, envelope, slot, this.logger); await this.api.beacon.publishExecutionPayloadEnvelope({ signedExecutionPayloadEnvelope: signedEnvelope, diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 05d3626e6557..7a7b91be0059 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -214,10 +214,6 @@ export class ValidatorStore { return this.indicesService.index2pubkey.get(index); } - getValidatorIndex(pubkeyHex: PubkeyHex): ValidatorIndex | undefined { - return this.indicesService.getValidatorIndex(pubkeyHex); - } - pollValidatorIndices(): Promise { // Consumers will call this function every epoch forever. If everyone has been discovered, skip return this.indicesService.indexCount >= this.validators.size @@ -500,20 +496,24 @@ export class ValidatorStore { async signExecutionPayloadEnvelope( pubkey: BLSPubkey, envelope: gloas.ExecutionPayloadEnvelope, - currentSlot: Slot + currentSlot: Slot, + logger?: LoggerVc ): Promise { // Make sure the envelope slot is not higher than the current slot to avoid potential attacks. if (envelope.slot > currentSlot) { throw Error(`Not signing envelope with slot ${envelope.slot} greater than current slot ${currentSlot}`); } - // Duties are filtered before-hard by doppelganger-safe, this assert should never throw - this.assertDoppelgangerSafe(pubkey); - const signingSlot = envelope.slot; const domain = this.config.getDomain(signingSlot, DOMAIN_BEACON_BUILDER); const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadEnvelope, envelope, domain); + logger?.debug("Signing execution payload envelope", { + slot: signingSlot, + beaconBlockRoot: toRootHex(envelope.beaconBlockRoot), + signingRoot: toRootHex(signingRoot), + }); + const signableMessage: SignableMessage = { type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE, data: envelope, From 3756e733e0e05e2b018e893320e25bcedece75ac Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 11:17:57 +0000 Subject: [PATCH 17/34] Rephrase comment --- .../beacon-node/src/chain/produceBlock/produceBlockBody.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 07f440de7007..663c0a972b5f 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -114,8 +114,7 @@ export type ProduceFullGloas = { /** * Cached envelope state root computed during block production. * This is the state root after running `processExecutionPayloadEnvelope` on the - * post-block state, avoiding an expensive state clone when the validator API - * later constructs the `ExecutionPayloadEnvelope`. + * post-block state, and later used to construct the `ExecutionPayloadEnvelope`. */ envelopeStateRoot: Root; }; From d1a37efe48f3d5f1642feac42a1c4e49c629bdcb Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 11:20:22 +0000 Subject: [PATCH 18/34] Clean up comments --- packages/beacon-node/src/api/impl/beacon/blocks/index.ts | 2 +- packages/validator/src/services/validatorStore.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) 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 888ec0251c3d..b77d862bc654 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -711,7 +711,7 @@ export function getBeaconBlockApi({ const publishPromises = [ // Gossip the signed execution payload envelope first () => network.publishSignedExecutionPayloadEnvelope(signedExecutionPayloadEnvelope), - // For self-builds, publish all DataColumnSidecars + // For self-builds, publish all data column sidecars ...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)), ]; diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 7a7b91be0059..1c461af9d36d 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -489,10 +489,6 @@ export class ValidatorStore { } as SignedBeaconBlock | SignedBlindedBeaconBlock; } - /** - * Sign an execution payload envelope for Gloas self-building. - * Uses DOMAIN_BEACON_BUILDER domain as per the spec. - */ async signExecutionPayloadEnvelope( pubkey: BLSPubkey, envelope: gloas.ExecutionPayloadEnvelope, From 7bc2ac87c5e95c67810d824d616a1d4abb7e4100 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 11:28:53 +0000 Subject: [PATCH 19/34] Review block proposing service --- packages/validator/src/services/block.ts | 54 ++++++++++++++++-------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index a28a28356751..64317f3f4e68 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -180,22 +180,28 @@ export class BlockProposingService { */ private async createAndPublishBlockGloas(pubkey: BLSPubkey, slot: Slot): Promise { const pubkeyHex = toPubkeyHex(pubkey); + const logCtx = {slot, validator: prettyBytes(pubkeyHex)}; const debugLogCtx = {slot, validator: pubkeyHex}; const randaoReveal = await this.validatorStore.signRandao(pubkey, slot); const graffiti = this.validatorStore.getGraffiti(pubkeyHex); const feeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex); - this.logger.debug("Producing Gloas block", {...debugLogCtx, feeRecipient}); + this.logger.debug("Producing block", {...debugLogCtx, feeRecipient}); this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot)); // Step 1: Produce beacon block with execution payload bid - const blockRes = await this.api.validator.produceBlockV4({ - slot, - randaoReveal, - graffiti, - feeRecipient, - }); + const blockRes = await this.api.validator + .produceBlockV4({ + slot, + randaoReveal, + graffiti, + feeRecipient, + }) + .catch((e: Error) => { + this.metrics?.blockProposingErrors.inc({error: "produce"}); + throw extendError(e, "Failed to produce block"); + }); const block = blockRes.value(); const blockMeta = blockRes.meta(); @@ -209,12 +215,19 @@ export class BlockProposingService { const signedBlock = await this.validatorStore.signBlock(pubkey, block, slot, this.logger); const {broadcastValidation} = this.opts; - await this.api.beacon.publishBlockV2({ - signedBlockContents: {signedBlock}, - broadcastValidation, - }); + ( + await this.api.beacon + .publishBlockV2({ + signedBlockContents: {signedBlock}, + broadcastValidation, + }) + .catch((e: Error) => { + this.metrics?.blockProposingErrors.inc({error: "publish"}); + throw extendError(e, "Failed to publish block"); + }) + ).assertOk(); - this.logger.debug("Published Gloas beacon block", debugLogCtx); + this.logger.debug("Published beacon block", debugLogCtx); // Step 3: Get the execution payload envelope const beaconBlockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block); @@ -230,14 +243,21 @@ export class BlockProposingService { // Step 4: Sign and publish the envelope const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(pubkey, envelope, slot, this.logger); - await this.api.beacon.publishExecutionPayloadEnvelope({ - signedExecutionPayloadEnvelope: signedEnvelope, - }); + ( + await this.api.beacon + .publishExecutionPayloadEnvelope({ + signedExecutionPayloadEnvelope: signedEnvelope, + }) + .catch((e: Error) => { + this.metrics?.blockProposingErrors.inc({error: "publish"}); + throw extendError(e, "Failed to publish execution payload envelope"); + }) + ).assertOk(); this.metrics?.proposerStepCallPublishBlock.observe(this.clock.secFromSlot(slot)); this.metrics?.blocksPublished.inc(); - this.logger.info("Published Gloas block and envelope", { - ...debugLogCtx, + this.logger.info("Published block and execution payload envelope", { + ...logCtx, graffiti, consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue), }); From 93f801f29277d107e6a0149dee1143831cee1b49 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 11:29:01 +0000 Subject: [PATCH 20/34] Review produceBlockV4 --- .../src/api/impl/validator/index.ts | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 0898ced0de5f..173b7e0f0a64 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -914,12 +914,16 @@ export function getValidatorApi( notWhileSyncing(); await waitForSlot(slot); + // TODO GLOAS: support producing blocks from builder bids + const source = ProducedBlockSource.engine; + // TODO GLOAS: needs to be updated after fork choice changes are merged const parentBlock = chain.getProposerHead(slot); const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock; const parentBlockRoot = fromHex(parentBlockRootHex); notOnOutOfRangeData(parentBlockRoot); metrics?.blockProductionSlotDelta.set(slot - parentSlot); + metrics?.blockProductionRequests.inc({source}); const graffitiBytes = toGraffitiBytes( graffiti ?? getDefaultGraffiti(getLodestarClientVersion(), chain.executionEngine.clientVersion, {}) @@ -931,24 +935,44 @@ export function getValidatorApi( graffiti: graffitiBytes, }); - const {block, consensusBlockValue} = await chain.produceBlock({ - slot, - parentBlock, - randaoReveal, - graffiti: graffitiBytes, - feeRecipient, - commonBlockBodyPromise, - }); + let timer: undefined | ((opts: {source: ProducedBlockSource}) => number); + try { + timer = metrics?.blockProductionTime.startTimer(); + const {block, executionPayloadValue, consensusBlockValue} = await chain.produceBlock({ + slot, + parentBlock, + randaoReveal, + graffiti: graffitiBytes, + feeRecipient, + commonBlockBodyPromise, + }); - metrics?.blockProductionSuccess.inc({source: ProducedBlockSource.engine}); + metrics?.blockProductionSuccess.inc({source}); + metrics?.blockProductionNumAggregated.observe({source}, block.body.attestations.length); + metrics?.blockProductionConsensusBlockValue.observe({source}, Number(formatWeiToEth(consensusBlockValue))); + metrics?.blockProductionExecutionPayloadValue.observe({source}, Number(formatWeiToEth(executionPayloadValue))); - return { - data: block as gloas.BeaconBlock, - meta: { - version: fork, + const blockRoot = toRootHex(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block)); + logger.verbose("Produced block", { + slot, + executionPayloadValue, consensusBlockValue, - }, - }; + root: blockRoot, + }); + if (chain.opts.persistProducedBlocks) { + void chain.persistBlock(block, "produced_engine_block"); + } + + return { + data: block as gloas.BeaconBlock, + meta: { + version: fork, + consensusBlockValue, + }, + }; + } finally { + if (timer) timer({source}); + } }, async produceAttestationData({committeeIndex, slot}) { From 30bdc9757397ce1ef8edcdbd1cdb94f5beea4f55 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 11:36:08 +0000 Subject: [PATCH 21/34] Add todo --- packages/beacon-node/src/chain/options.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index e25a7b86a40e..1923b137299e 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -27,6 +27,7 @@ export type IChainOptions = BlockProcessOpts & blsVerifyAllMainThread?: boolean; blsVerifyAllMultiThread?: boolean; blacklistedBlocks?: string[]; + // TODO GLOAS: add similar option for execution payload envelopes? persistProducedBlocks?: boolean; persistInvalidSszObjects?: boolean; persistInvalidSszObjectsDir?: string; From 83ca6421762b367c5f8c81b5e8d37962e986410e Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 11:47:47 +0000 Subject: [PATCH 22/34] Restore comment --- packages/beacon-node/src/chain/chain.ts | 1 + packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 59518f716224..1b0e9d20a19c 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -980,6 +980,7 @@ export class BeaconChain implements IBeaconChain { gloasResult.envelopeStateRoot = envelopeStateRoot; } + // Track the produced block for consensus broadcast validations, later validation, etc. this.blockProductionCache.set(blockRootHex, produceResult); this.metrics?.blockProductionCacheSize.set(this.blockProductionCache.size); diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 663c0a972b5f..e495c8a0b170 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -279,7 +279,7 @@ export async function produceBlockBody( gloasBody.payloadAttestations = []; blockBody = gloasBody as AssembledBodyType; - // Store execution payload data in produceResult for envelope creation later + // Store execution payload data required to construct execution payload envelope later const gloasResult = produceResult as ProduceFullGloas; gloasResult.executionPayload = executionPayload as ExecutionPayload; gloasResult.executionRequests = executionRequests; From ac5d22a14178cff46564128f57f0d8603c8ea6a5 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 11:56:21 +0000 Subject: [PATCH 23/34] comments --- packages/beacon-node/src/util/dataColumns.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/src/util/dataColumns.ts b/packages/beacon-node/src/util/dataColumns.ts index c486755119ea..11748bfc74a1 100644 --- a/packages/beacon-node/src/util/dataColumns.ts +++ b/packages/beacon-node/src/util/dataColumns.ts @@ -280,7 +280,7 @@ export function getBlobKzgCommitments( return (signedBlock.message.body as BeaconBlockBody).blobKzgCommitments; } -/** Type guard for gloas.DataColumnSidecar */ +/** Type guard for `gloas.DataColumnSidecar` */ export function isGloasDataColumnSidecar(sidecar: DataColumnSidecar): sidecar is gloas.DataColumnSidecar { return (sidecar as gloas.DataColumnSidecar).beaconBlockRoot !== undefined; } @@ -368,8 +368,7 @@ export function getDataColumnSidecarsFromColumnSidecar( } /** - * For GLOAS self-builds: build DataColumnSidecars from the envelope data. - * In GLOAS, DataColumnSidecar has a simplified structure with `slot` and `beaconBlockRoot` + * In Gloas, data column sidecars have a simplified structure with `slot` and `beaconBlockRoot` * instead of `signedBlockHeader`, `kzgCommitments`, and `kzgCommitmentsInclusionProof`. */ export function getDataColumnSidecarsForGloas( From 9480e670ca513edfa0f4510f6ba5ed778be5859c Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 14 Feb 2026 12:11:18 +0000 Subject: [PATCH 24/34] Review vc block production --- packages/validator/src/services/block.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index 64317f3f4e68..fac8573b01c2 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -13,7 +13,7 @@ import { Slot, isBlindedSignedBeaconBlock, } from "@lodestar/types"; -import {extendError, prettyBytes, prettyWeiToEth, toPubkeyHex} from "@lodestar/utils"; +import {extendError, prettyBytes, prettyWeiToEth, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {Metrics} from "../metrics.js"; import {PubkeyHex} from "../types.js"; import {IClock, LoggerVc} from "../util/index.js"; @@ -172,7 +172,7 @@ export class BlockProposingService { } /** - * Gloas block production flow: + * Gloas stateful block production flow: * 1. Produce beacon block with execution payload bid * 2. Sign and publish the beacon block * 3. Get the execution payload envelope @@ -204,10 +204,13 @@ export class BlockProposingService { }); const block = blockRes.value(); const blockMeta = blockRes.meta(); + const beaconBlockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block); + const blockRootHex = toRootHex(beaconBlockRoot); - this.logger.debug("Produced Gloas block", { + this.logger.debug("Produced block", { ...debugLogCtx, consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue), + blockRoot: blockRootHex, }); this.metrics?.blocksProduced.inc(); @@ -215,6 +218,9 @@ export class BlockProposingService { const signedBlock = await this.validatorStore.signBlock(pubkey, block, slot, this.logger); const {broadcastValidation} = this.opts; + // TODO GLOAS: we should be able to publish block and execution payload in parallel + // however for devnet-0 it's unclear if all clients have implemented queuing of the + // execution payload on gossip and might ignore it if the receive it before the block ( await this.api.beacon .publishBlockV2({ @@ -227,18 +233,18 @@ export class BlockProposingService { }) ).assertOk(); - this.logger.debug("Published beacon block", debugLogCtx); + this.logger.debug("Published beacon block", {...debugLogCtx, broadcastValidation}); // Step 3: Get the execution payload envelope - const beaconBlockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block); const envelopeRes = await this.api.validator.getExecutionPayloadEnvelope({ slot, beaconBlockRoot, builderIndex: BUILDER_INDEX_SELF_BUILD, }); const envelope = envelopeRes.value(); + const stateRootHex = toRootHex(envelope.stateRoot); - this.logger.debug("Retrieved execution payload envelope", debugLogCtx); + this.logger.debug("Retrieved execution payload envelope", {...debugLogCtx, stateRoot: stateRootHex}); // Step 4: Sign and publish the envelope const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(pubkey, envelope, slot, this.logger); @@ -260,6 +266,7 @@ export class BlockProposingService { ...logCtx, graffiti, consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue), + blockRoot: blockRootHex, }); } From fa1e11dda76628e4b29ec2c076fa722aaf1b2427 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sun, 15 Feb 2026 11:55:11 +0000 Subject: [PATCH 25/34] Improve cache assertions in publishExecutionPayloadEnvelope --- .../beacon-node/src/api/impl/beacon/blocks/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 b77d862bc654..fd0bc0ec2b56 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -659,7 +659,17 @@ export function getBeaconBlockApi({ if (isSelfBuild) { // For self-builds, construct and publish data column sidecars from cached block production data const cachedResult = chain.blockProductionCache.get(blockRootHex) as ProduceFullGloas | undefined; - if (cachedResult?.cells && cachedResult.blobsBundle.commitments.length > 0) { + if (cachedResult === undefined) { + throw new ApiError(404, `No cached block production result found for block root ${blockRootHex}`); + } + if (!isForkPostGloas(cachedResult.fork)) { + throw new ApiError(400, `Cached block production result is for pre-gloas fork=${cachedResult.fork}`); + } + if (cachedResult.type !== BlockType.Full) { + throw new ApiError(400, "Cached block production result is not full block"); + } + + if (cachedResult.cells && cachedResult.blobsBundle.commitments.length > 0) { const cellsAndProofs = cachedResult.cells.map((rowCells, rowIndex) => ({ cells: rowCells, proofs: cachedResult.blobsBundle.proofs.slice( From 66fc969e04ac4f21d4519d3eb306d0affb44abfa Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Mon, 16 Feb 2026 13:03:43 +0000 Subject: [PATCH 26/34] Fix serialization of builder index over the wire --- packages/api/src/beacon/routes/validator.ts | 27 ++++++++++++++++++--- packages/config/src/chainConfig/json.ts | 13 +++++----- packages/params/src/index.ts | 1 + 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index e46510175860..ac2dcb8514b8 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -4,6 +4,7 @@ import { ForkPostDeneb, ForkPostGloas, ForkPreDeneb, + MAX_UINT64_STR, VALIDATOR_REGISTRY_LIMIT, isForkPostDeneb, isForkPostElectra, @@ -420,7 +421,7 @@ export type Endpoints = { /** Index of the builder from which the execution payload envelope is requested */ builderIndex: ValidatorIndex; }, - {params: {slot: Slot; beacon_block_root: string; builder_index: ValidatorIndex}}, + {params: {slot: Slot; beacon_block_root: string; builder_index: string}}, gloas.ExecutionPayloadEnvelope, VersionMeta >; @@ -900,18 +901,22 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({ - params: {slot, beacon_block_root: toRootHex(beaconBlockRoot), builder_index: builderIndex}, + params: { + slot, + beacon_block_root: toRootHex(beaconBlockRoot), + builder_index: serializeBuilderIndex(builderIndex), + }, }), parseReq: ({params}) => ({ slot: params.slot, beaconBlockRoot: fromHex(params.beacon_block_root), - builderIndex: params.builder_index, + builderIndex: deserializeBuilderIndex(params.builder_index), }), schema: { params: { slot: Schema.UintRequired, beacon_block_root: Schema.StringRequired, - builder_index: Schema.UintRequired, + builder_index: Schema.StringRequired, }, }, }, @@ -1211,6 +1216,20 @@ function parseBuilderBoostFactor(builderBoostFactorInput?: string | number | big return builderBoostFactorInput !== undefined ? BigInt(builderBoostFactorInput) : undefined; } +function serializeBuilderIndex(builderIndex: number): string { + if (builderIndex === Infinity) { + return MAX_UINT64_STR; + } + return builderIndex.toString(10); +} + +function deserializeBuilderIndex(builderIndexInput: string | number): number { + if (String(builderIndexInput) === MAX_UINT64_STR) { + return Infinity; + } + return Number(builderIndexInput); +} + function writeSkipRandaoVerification(skipRandaoVerification?: boolean): string | undefined { return skipRandaoVerification === true ? "" : undefined; } diff --git a/packages/config/src/chainConfig/json.ts b/packages/config/src/chainConfig/json.ts index 693d0516c4de..12f4cdbd57c3 100644 --- a/packages/config/src/chainConfig/json.ts +++ b/packages/config/src/chainConfig/json.ts @@ -1,3 +1,4 @@ +import {MAX_UINT64_STR} from "@lodestar/params"; import {fromHex, toHex} from "@lodestar/utils"; import {validateBlobSchedule} from "../utils/validateBlobSchedule.js"; import { @@ -11,8 +12,6 @@ import { isBlobSchedule, } from "./types.js"; -const MAX_UINT64_JSON = "18446744073709551615"; - export function chainConfigToJson(config: ChainConfig): SpecJson { const json: SpecJson = {}; @@ -69,7 +68,7 @@ export function serializeSpecValue( throw Error(`Invalid value ${value.toString()} expected number`); } if (value === Infinity) { - return MAX_UINT64_JSON; + return MAX_UINT64_STR; } return value.toString(10); @@ -97,8 +96,8 @@ export function serializeSpecValue( } return value.map(({EPOCH, MAX_BLOBS_PER_BLOCK}) => ({ - EPOCH: EPOCH === Infinity ? MAX_UINT64_JSON : EPOCH.toString(10), - MAX_BLOBS_PER_BLOCK: MAX_BLOBS_PER_BLOCK === Infinity ? MAX_UINT64_JSON : MAX_BLOBS_PER_BLOCK.toString(10), + EPOCH: EPOCH === Infinity ? MAX_UINT64_STR : EPOCH.toString(10), + MAX_BLOBS_PER_BLOCK: MAX_BLOBS_PER_BLOCK === Infinity ? MAX_UINT64_STR : MAX_BLOBS_PER_BLOCK.toString(10), })); } } @@ -114,7 +113,7 @@ export function deserializeSpecValue(valueStr: unknown, typeName: SpecValueTypeN switch (typeName) { case "number": - if (valueStr === MAX_UINT64_JSON) { + if (valueStr === MAX_UINT64_STR) { return Infinity; } return parseInt(valueStr, 10); @@ -153,7 +152,7 @@ export function deserializeBlobSchedule(input: unknown): BlobSchedule { throw Error(`Invalid BLOB_SCHEDULE[${i}].${key} value ${value} expected string`); } - if (value === MAX_UINT64_JSON) { + if (value === MAX_UINT64_STR) { out[key] = Infinity; } else { const parsed = parseInt(value, 10); diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 34bc96977b4f..590653284c71 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -133,6 +133,7 @@ export const { export const GENESIS_SLOT = 0; export const GENESIS_EPOCH = 0; export const FAR_FUTURE_EPOCH = Infinity; +export const MAX_UINT64_STR = "18446744073709551615"; export const BASE_REWARDS_PER_EPOCH = 4; export const DEPOSIT_CONTRACT_TREE_DEPTH = 2 ** 5; // 32 export const JUSTIFICATION_BITS_LENGTH = 4; From 6a33fac552621b57d9acdbf1fbd04e4d0c055bcf Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Mon, 16 Feb 2026 13:06:45 +0000 Subject: [PATCH 27/34] Use BUILDER_INDEX_SELF_BUILD const --- packages/api/src/beacon/routes/validator.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index ac2dcb8514b8..653476741f96 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -1,6 +1,7 @@ import {ContainerType, Type, ValueOf} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import { + BUILDER_INDEX_SELF_BUILD, ForkPostDeneb, ForkPostGloas, ForkPreDeneb, @@ -1217,7 +1218,7 @@ function parseBuilderBoostFactor(builderBoostFactorInput?: string | number | big } function serializeBuilderIndex(builderIndex: number): string { - if (builderIndex === Infinity) { + if (builderIndex === BUILDER_INDEX_SELF_BUILD) { return MAX_UINT64_STR; } return builderIndex.toString(10); @@ -1225,7 +1226,7 @@ function serializeBuilderIndex(builderIndex: number): string { function deserializeBuilderIndex(builderIndexInput: string | number): number { if (String(builderIndexInput) === MAX_UINT64_STR) { - return Infinity; + return BUILDER_INDEX_SELF_BUILD; } return Number(builderIndexInput); } From 85655d5000b260211f91e2a93c46a83d9ad31745 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Mon, 16 Feb 2026 13:11:52 +0000 Subject: [PATCH 28/34] Use BuilderIndex type --- packages/api/src/beacon/routes/validator.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 653476741f96..f662c7ab0871 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -17,6 +17,7 @@ import { BeaconBlock, BlindedBeaconBlock, BlockContents, + BuilderIndex, CommitteeIndex, Epoch, ProducedBlockSource, @@ -420,7 +421,7 @@ export type Endpoints = { /** Root of the beacon block that this envelope is for */ beaconBlockRoot: Root; /** Index of the builder from which the execution payload envelope is requested */ - builderIndex: ValidatorIndex; + builderIndex: BuilderIndex; }, {params: {slot: Slot; beacon_block_root: string; builder_index: string}}, gloas.ExecutionPayloadEnvelope, @@ -1217,14 +1218,14 @@ function parseBuilderBoostFactor(builderBoostFactorInput?: string | number | big return builderBoostFactorInput !== undefined ? BigInt(builderBoostFactorInput) : undefined; } -function serializeBuilderIndex(builderIndex: number): string { +function serializeBuilderIndex(builderIndex: BuilderIndex): string { if (builderIndex === BUILDER_INDEX_SELF_BUILD) { return MAX_UINT64_STR; } return builderIndex.toString(10); } -function deserializeBuilderIndex(builderIndexInput: string | number): number { +function deserializeBuilderIndex(builderIndexInput: string | number): BuilderIndex { if (String(builderIndexInput) === MAX_UINT64_STR) { return BUILDER_INDEX_SELF_BUILD; } From eccf56f3a150ded19ad4f846e685a2305035ebf6 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 19 Feb 2026 10:02:14 +0000 Subject: [PATCH 29/34] Add comment --- packages/beacon-node/src/api/impl/beacon/blocks/index.ts | 1 + 1 file changed, 1 insertion(+) 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 fd0bc0ec2b56..c7a860c50820 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -313,6 +313,7 @@ export function getBeaconBlockApi({ ]; const sentPeersArr = await promiseAllMaybeAsync(publishPromises); + // After gloas, data columns are not published with the block but when publishing the execution payload envelope if (isForkPostFulu(fork) && !isForkPostGloas(fork)) { let columnsPublishedWithZeroPeers = 0; // sent peers per topic are logged in network.publishGossip(), here we only track metrics for it From a37885642c24c9ece33c48f8c93ce0b61d30345b Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 19 Feb 2026 10:18:56 +0000 Subject: [PATCH 30/34] Check slot consistency of envelope during publishing --- .../src/api/impl/beacon/blocks/index.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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 c7a860c50820..525f71891cca 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -640,18 +640,20 @@ export function getBeaconBlockApi({ async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}) { const seenTimestampSec = Date.now() / 1000; const envelope = signedExecutionPayloadEnvelope.message; - const slot = envelope.slot; - const fork = config.getForkName(slot); + const fork = config.getForkName(envelope.slot); const blockRootHex = toRootHex(envelope.beaconBlockRoot); if (!isForkPostGloas(fork)) { throw new ApiError(400, `publishExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`); } - // Validate that we're not too far in the future - const currentSlot = chain.clock.currentSlot; - if (slot > currentSlot + 1) { - throw new ApiError(400, `Envelope slot ${slot} is too far in the future (current: ${currentSlot})`); + // TODO GLOAS: review checks, do we want to implement `broadcast_validation`? + const block = chain.forkChoice.getBlockHex(blockRootHex); + if (block === null) { + throw new ApiError(404, `Block not found for beacon block root ${blockRootHex}`); + } + if (block.slot !== envelope.slot) { + throw new ApiError(400, `Envelope slot ${envelope.slot} does not match block slot ${block.slot}`); } const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; @@ -679,7 +681,7 @@ export function getBeaconBlockApi({ ), })); - dataColumnSidecars = getDataColumnSidecarsForGloas(slot, envelope.beaconBlockRoot, cellsAndProofs); + dataColumnSidecars = getDataColumnSidecarsForGloas(envelope.slot, envelope.beaconBlockRoot, cellsAndProofs); } } else { // TODO GLOAS: will this api be used by builders or only for self-building? @@ -741,7 +743,7 @@ export function getBeaconBlockApi({ } if (columnsPublishedWithZeroPeers > 0) { chain.logger.warn("Published data columns to 0 peers, increased risk of reorg", { - slot, + slot: envelope.slot, blockRoot: blockRootHex, columns: columnsPublishedWithZeroPeers, }); @@ -756,7 +758,7 @@ export function getBeaconBlockApi({ for (const dataColumnSidecar of dataColumnSidecars) { chain.emitter.emit(routes.events.EventType.dataColumnSidecar, { blockRoot: blockRootHex, - slot, + slot: envelope.slot, index: dataColumnSidecar.index, kzgCommitments, }); From d1611c7885a0e6b6e0c5315efc581d16ebbd17c2 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 19 Feb 2026 10:33:42 +0000 Subject: [PATCH 31/34] Remove builder index when getting the envelope --- packages/api/src/beacon/routes/validator.ts | 30 +++---------------- .../test/unit/beacon/testData/validator.ts | 2 +- .../src/api/impl/beacon/blocks/index.ts | 13 ++++---- .../src/api/impl/validator/index.ts | 9 ++---- packages/validator/src/services/block.ts | 3 +- 5 files changed, 15 insertions(+), 42 deletions(-) diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index f662c7ab0871..6382a452f1c3 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -1,11 +1,9 @@ import {ContainerType, Type, ValueOf} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import { - BUILDER_INDEX_SELF_BUILD, ForkPostDeneb, ForkPostGloas, ForkPreDeneb, - MAX_UINT64_STR, VALIDATOR_REGISTRY_LIMIT, isForkPostDeneb, isForkPostElectra, @@ -17,7 +15,6 @@ import { BeaconBlock, BlindedBeaconBlock, BlockContents, - BuilderIndex, CommitteeIndex, Epoch, ProducedBlockSource, @@ -410,7 +407,7 @@ export type Endpoints = { /** * Get execution payload envelope. - * Retrieves execution payload envelope for a given slot and builder. + * Retrieves execution payload envelope for a given slot and beacon block root. * The envelope contains the full execution payload along with associated metadata. */ getExecutionPayloadEnvelope: Endpoint< @@ -420,10 +417,8 @@ export type Endpoints = { slot: Slot; /** Root of the beacon block that this envelope is for */ beaconBlockRoot: Root; - /** Index of the builder from which the execution payload envelope is requested */ - builderIndex: BuilderIndex; }, - {params: {slot: Slot; beacon_block_root: string; builder_index: string}}, + {params: {slot: Slot; beacon_block_root: string}}, gloas.ExecutionPayloadEnvelope, VersionMeta >; @@ -899,26 +894,23 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({ + writeReq: ({slot, beaconBlockRoot}) => ({ params: { slot, beacon_block_root: toRootHex(beaconBlockRoot), - builder_index: serializeBuilderIndex(builderIndex), }, }), parseReq: ({params}) => ({ slot: params.slot, beaconBlockRoot: fromHex(params.beacon_block_root), - builderIndex: deserializeBuilderIndex(params.builder_index), }), schema: { params: { slot: Schema.UintRequired, beacon_block_root: Schema.StringRequired, - builder_index: Schema.StringRequired, }, }, }, @@ -1218,20 +1210,6 @@ function parseBuilderBoostFactor(builderBoostFactorInput?: string | number | big return builderBoostFactorInput !== undefined ? BigInt(builderBoostFactorInput) : undefined; } -function serializeBuilderIndex(builderIndex: BuilderIndex): string { - if (builderIndex === BUILDER_INDEX_SELF_BUILD) { - return MAX_UINT64_STR; - } - return builderIndex.toString(10); -} - -function deserializeBuilderIndex(builderIndexInput: string | number): BuilderIndex { - if (String(builderIndexInput) === MAX_UINT64_STR) { - return BUILDER_INDEX_SELF_BUILD; - } - return Number(builderIndexInput); -} - function writeSkipRandaoVerification(skipRandaoVerification?: boolean): string | undefined { return skipRandaoVerification === true ? "" : undefined; } diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index 0e48b3085746..6083b6566a5c 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -92,7 +92,7 @@ export const testData: GenericServerTestCases = { }, }, getExecutionPayloadEnvelope: { - args: {slot: 32000, beaconBlockRoot: ZERO_HASH, builderIndex: 1}, + args: {slot: 32000, beaconBlockRoot: ZERO_HASH}, res: { data: ssz.gloas.ExecutionPayloadEnvelope.defaultValue(), meta: {version: ForkName.gloas}, 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 525f71891cca..10d048d7d768 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -640,7 +640,8 @@ export function getBeaconBlockApi({ async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}) { const seenTimestampSec = Date.now() / 1000; const envelope = signedExecutionPayloadEnvelope.message; - const fork = config.getForkName(envelope.slot); + const slot = envelope.slot; + const fork = config.getForkName(slot); const blockRootHex = toRootHex(envelope.beaconBlockRoot); if (!isForkPostGloas(fork)) { @@ -652,8 +653,8 @@ export function getBeaconBlockApi({ if (block === null) { throw new ApiError(404, `Block not found for beacon block root ${blockRootHex}`); } - if (block.slot !== envelope.slot) { - throw new ApiError(400, `Envelope slot ${envelope.slot} does not match block slot ${block.slot}`); + if (block.slot !== slot) { + throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`); } const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; @@ -681,7 +682,7 @@ export function getBeaconBlockApi({ ), })); - dataColumnSidecars = getDataColumnSidecarsForGloas(envelope.slot, envelope.beaconBlockRoot, cellsAndProofs); + dataColumnSidecars = getDataColumnSidecarsForGloas(slot, envelope.beaconBlockRoot, cellsAndProofs); } } else { // TODO GLOAS: will this api be used by builders or only for self-building? @@ -743,7 +744,7 @@ export function getBeaconBlockApi({ } if (columnsPublishedWithZeroPeers > 0) { chain.logger.warn("Published data columns to 0 peers, increased risk of reorg", { - slot: envelope.slot, + slot, blockRoot: blockRootHex, columns: columnsPublishedWithZeroPeers, }); @@ -758,7 +759,7 @@ export function getBeaconBlockApi({ for (const dataColumnSidecar of dataColumnSidecars) { chain.emitter.emit(routes.events.EventType.dataColumnSidecar, { blockRoot: blockRootHex, - slot: envelope.slot, + slot, index: dataColumnSidecar.index, kzgCommitments, }); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 173b7e0f0a64..adbba9e2fbdf 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1607,7 +1607,7 @@ export function getValidatorApi( }); }, - async getExecutionPayloadEnvelope({slot, beaconBlockRoot, builderIndex}) { + async getExecutionPayloadEnvelope({slot, beaconBlockRoot}) { const fork = config.getForkName(slot); if (!isForkPostGloas(fork)) { @@ -1617,11 +1617,6 @@ export function getValidatorApi( notWhileSyncing(); await waitForSlot(slot); - // TODO GLOAS: add support for acting as builder - if (builderIndex !== BUILDER_INDEX_SELF_BUILD) { - throw new ApiError(400, `Builder index must be BUILDER_INDEX_SELF_BUILD but got ${builderIndex}`); - } - const blockRootHex = toRootHex(beaconBlockRoot); const produceResult = chain.blockProductionCache.get(blockRootHex); @@ -1649,7 +1644,7 @@ export function getValidatorApi( logger.info("Produced execution payload envelope", { slot, blockRoot: blockRootHex, - builderIndex, + builderIndex: envelope.builderIndex, transactions: executionPayload.transactions.length, blockHash: toRootHex(executionPayload.blockHash), }); diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index fac8573b01c2..a3f198273d5e 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -1,6 +1,6 @@ import {ApiClient, routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; -import {BUILDER_INDEX_SELF_BUILD, isForkPostGloas} from "@lodestar/params"; +import {isForkPostGloas} from "@lodestar/params"; import { BLSPubkey, BLSSignature, @@ -239,7 +239,6 @@ export class BlockProposingService { const envelopeRes = await this.api.validator.getExecutionPayloadEnvelope({ slot, beaconBlockRoot, - builderIndex: BUILDER_INDEX_SELF_BUILD, }); const envelope = envelopeRes.value(); const stateRootHex = toRootHex(envelope.stateRoot); From f2031cc3a56d0f9bd402dbb8b7cba0c4743531cc Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 19 Feb 2026 10:42:01 +0000 Subject: [PATCH 32/34] Remove builder index from log --- packages/beacon-node/src/api/impl/validator/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index adbba9e2fbdf..0b4f6767d6c7 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1644,7 +1644,6 @@ export function getValidatorApi( logger.info("Produced execution payload envelope", { slot, blockRoot: blockRootHex, - builderIndex: envelope.builderIndex, transactions: executionPayload.transactions.length, blockHash: toRootHex(executionPayload.blockHash), }); From 21ae1b8971268c8578a29179cc8024d08d79cc3a Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 19 Feb 2026 10:42:25 +0000 Subject: [PATCH 33/34] Revert some changes in params --- packages/config/src/chainConfig/json.ts | 13 +++++++------ packages/params/src/index.ts | 1 - 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/config/src/chainConfig/json.ts b/packages/config/src/chainConfig/json.ts index 12f4cdbd57c3..693d0516c4de 100644 --- a/packages/config/src/chainConfig/json.ts +++ b/packages/config/src/chainConfig/json.ts @@ -1,4 +1,3 @@ -import {MAX_UINT64_STR} from "@lodestar/params"; import {fromHex, toHex} from "@lodestar/utils"; import {validateBlobSchedule} from "../utils/validateBlobSchedule.js"; import { @@ -12,6 +11,8 @@ import { isBlobSchedule, } from "./types.js"; +const MAX_UINT64_JSON = "18446744073709551615"; + export function chainConfigToJson(config: ChainConfig): SpecJson { const json: SpecJson = {}; @@ -68,7 +69,7 @@ export function serializeSpecValue( throw Error(`Invalid value ${value.toString()} expected number`); } if (value === Infinity) { - return MAX_UINT64_STR; + return MAX_UINT64_JSON; } return value.toString(10); @@ -96,8 +97,8 @@ export function serializeSpecValue( } return value.map(({EPOCH, MAX_BLOBS_PER_BLOCK}) => ({ - EPOCH: EPOCH === Infinity ? MAX_UINT64_STR : EPOCH.toString(10), - MAX_BLOBS_PER_BLOCK: MAX_BLOBS_PER_BLOCK === Infinity ? MAX_UINT64_STR : MAX_BLOBS_PER_BLOCK.toString(10), + EPOCH: EPOCH === Infinity ? MAX_UINT64_JSON : EPOCH.toString(10), + MAX_BLOBS_PER_BLOCK: MAX_BLOBS_PER_BLOCK === Infinity ? MAX_UINT64_JSON : MAX_BLOBS_PER_BLOCK.toString(10), })); } } @@ -113,7 +114,7 @@ export function deserializeSpecValue(valueStr: unknown, typeName: SpecValueTypeN switch (typeName) { case "number": - if (valueStr === MAX_UINT64_STR) { + if (valueStr === MAX_UINT64_JSON) { return Infinity; } return parseInt(valueStr, 10); @@ -152,7 +153,7 @@ export function deserializeBlobSchedule(input: unknown): BlobSchedule { throw Error(`Invalid BLOB_SCHEDULE[${i}].${key} value ${value} expected string`); } - if (value === MAX_UINT64_STR) { + if (value === MAX_UINT64_JSON) { out[key] = Infinity; } else { const parsed = parseInt(value, 10); diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 590653284c71..34bc96977b4f 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -133,7 +133,6 @@ export const { export const GENESIS_SLOT = 0; export const GENESIS_EPOCH = 0; export const FAR_FUTURE_EPOCH = Infinity; -export const MAX_UINT64_STR = "18446744073709551615"; export const BASE_REWARDS_PER_EPOCH = 4; export const DEPOSIT_CONTRACT_TREE_DEPTH = 2 ** 5; // 32 export const JUSTIFICATION_BITS_LENGTH = 4; From a118ebcc348025de6dd8290acd7682e4221377c2 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 20 Feb 2026 13:50:04 +0000 Subject: [PATCH 34/34] Apply review feedback --- packages/api/src/beacon/routes/beacon/block.ts | 4 ++-- packages/beacon-node/src/api/impl/beacon/blocks/index.ts | 7 ++++--- packages/beacon-node/src/api/impl/validator/index.ts | 2 +- packages/beacon-node/src/chain/chain.ts | 7 ++++++- .../src/chain/produceBlock/computeNewStateRoot.ts | 2 ++ .../beacon-node/src/chain/produceBlock/produceBlockBody.ts | 3 +++ packages/beacon-node/src/metrics/metrics/beacon.ts | 5 +++++ 7 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index 2450478d0b5f..119e5fe2cf49 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -429,7 +429,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ) : sszTypesFor(fork).SignedBeaconBlock.toJson( - signedBlockContents.signedBlock as SignedBeaconBlock + signedBlockContents.signedBlock as SignedBeaconBlock ), headers: { [MetaHeader.Version]: fork, @@ -458,7 +458,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ) : sszTypesFor(fork).SignedBeaconBlock.serialize( - signedBlockContents.signedBlock as SignedBeaconBlock + signedBlockContents.signedBlock as SignedBeaconBlock ), headers: { [MetaHeader.Version]: fork, 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 10d048d7d768..37d34f6c3345 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -313,8 +313,9 @@ export function getBeaconBlockApi({ ]; const sentPeersArr = await promiseAllMaybeAsync(publishPromises); - // After gloas, data columns are not published with the block but when publishing the execution payload envelope - if (isForkPostFulu(fork) && !isForkPostGloas(fork)) { + if (isForkPostGloas(fork)) { + // After gloas, data columns are not published with the block but when publishing the execution payload envelope + } else if (isForkPostFulu(fork)) { let columnsPublishedWithZeroPeers = 0; // sent peers per topic are logged in network.publishGossip(), here we only track metrics for it // starting from fulu, we have to push to 128 subnets so need to make sure we have enough sent peers per topic @@ -710,7 +711,7 @@ export function getBeaconBlockApi({ dataColumns: dataColumnSidecars.length, }; - // Simple implementation of a pending envelope queue. If envelope is a bit early, hold it. + // If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N. const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now(); if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) { await sleep(msToBlockSlot); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 0b4f6767d6c7..c2d759c73d78 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -971,7 +971,7 @@ export function getValidatorApi( }, }; } finally { - if (timer) timer({source}); + timer?.({source}); } }, diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 1b0e9d20a19c..7d4fa04f4004 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -966,7 +966,12 @@ export class BeaconChain implements IBeaconChain { : this.config.getPostBellatrixForkTypes(slot).BlindedBeaconBlock.hashTreeRoot(block as BlindedBeaconBlock); const blockRootHex = toRootHex(blockRoot); - if (isForkPostGloas(fork) && produceResult.type === BlockType.Full) { + if (isForkPostGloas(fork)) { + // TODO GLOAS: we should retire BlockType post-gloas, may need a new enum for self vs non-self built + if (produceResult.type !== BlockType.Full) { + throw Error(`Unexpected block type=${produceResult.type} for post-gloas fork=${fork}`); + } + const gloasResult = produceResult as ProduceFullGloas; const envelope: gloas.ExecutionPayloadEnvelope = { payload: gloasResult.executionPayload, diff --git a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts index 5bdbe879105a..55c44501db74 100644 --- a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts +++ b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts @@ -73,7 +73,9 @@ export function computeEnvelopeStateRoot( signature: G2_POINT_AT_INFINITY, }; + const processEnvelopeTimer = metrics?.blockPayload.executionPayloadEnvelopeProcessingTime.startTimer(); processExecutionPayloadEnvelope(postBlockState, signedEnvelope, false); + processEnvelopeTimer?.(); const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ source: StateHashTreeRootSource.computeEnvelopeStateRoot, diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index e495c8a0b170..76deccf25be0 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -200,6 +200,9 @@ export async function produceBlockBody( this.logger.verbose("Producing beacon block body", logMeta); if (isForkPostGloas(fork)) { + // TODO GLOAS: support non self-building here, the block type differentiation between + // full and blinded no longer makes sense in gloas, it might be a good idea to move + // this into a completely separate function and have pre/post gloas more separated const gloasState = currentState as CachedBeaconStateGloas; const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index 7ad773253bc4..09b5c89b7ee4 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -144,6 +144,11 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { help: "Time for preparing payload in advance", buckets: [0.1, 1, 3, 5, 10], }), + executionPayloadEnvelopeProcessingTime: register.histogram({ + name: "beacon_block_payload_envelope_processing_seconds", + help: "Time to process execution payload envelope during block production", + buckets: [0.005, 0.01, 0.05, 0.1, 0.2, 0.5, 1], + }), payloadFetchedTime: register.histogram<{prepType: PayloadPreparationType}>({ name: "beacon_block_payload_fetched_time", help: "Time to fetch the payload from EL",