diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index 29354d3c19f6..119e5fe2cf49 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -3,11 +3,13 @@ import {ChainForkConfig} from "@lodestar/config"; import { ForkName, ForkPostDeneb, + ForkPostGloas, ForkPreBellatrix, ForkPreDeneb, ForkPreElectra, isForkPostBellatrix, isForkPostDeneb, + isForkPostGloas, } from "@lodestar/params"; import { BeaconBlockBody, @@ -17,11 +19,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 +212,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 +423,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 +440,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 +452,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 +469,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 +589,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..6382a452f1c3 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 beacon block root. + * 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; + /** Root of the beacon block that this envelope is for */ + beaconBlockRoot: Root; + }, + {params: {slot: Slot; beacon_block_root: string}}, + gloas.ExecutionPayloadEnvelope, + VersionMeta + >; + /** * Produce an attestation data * Requests that the beacon node produce an AttestationData. @@ -763,6 +829,96 @@ 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}/{beacon_block_root}", + method: "GET", + req: { + writeReq: ({slot, beaconBlockRoot}) => ({ + params: { + slot, + beacon_block_root: toRootHex(beaconBlockRoot), + }, + }), + parseReq: ({params}) => ({ + slot: params.slot, + beaconBlockRoot: fromHex(params.beacon_block_root), + }), + schema: { + params: { + slot: Schema.UintRequired, + beacon_block_root: Schema.StringRequired, + }, + }, + }, + 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/genericServerTest/beacon.test.ts b/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts index 941f6fcb5a8a..4692ae296df2 100644 --- a/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts +++ b/packages/api/test/unit/beacon/genericServerTest/beacon.test.ts @@ -8,7 +8,7 @@ import {testData} from "../testData/beacon.js"; describe("beacon / beacon", () => { runGenericServerTest( - createChainForkConfig({...defaultChainConfig, ELECTRA_FORK_EPOCH: 0}), + createChainForkConfig({...defaultChainConfig, GLOAS_FORK_EPOCH: 0}), getClient, getRoutes, testData 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: diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 49fa3e0d8d0b..b470e98cbe03 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -75,7 +75,7 @@ export const testData: GenericServerTestCases = { }, publishBlockV2: { args: { - signedBlockContents: ssz.electra.SignedBlockContents.defaultValue(), + signedBlockContents: {signedBlock: ssz.gloas.SignedBeaconBlock.defaultValue()}, broadcastValidation: BroadcastValidation.consensus, }, res: undefined, @@ -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..6083b6566a5c 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, beaconBlockRoot: ZERO_HASH}, + 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 7b7ab22c13f9..37d34f6c3345 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, @@ -27,6 +28,7 @@ import { WithOptionalBytes, deneb, fulu, + gloas, isDenebBlockContents, sszTypesFor, } from "@lodestar/types"; @@ -42,6 +44,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 +54,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 +96,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, @@ -309,7 +313,9 @@ export function getBeaconBlockApi({ ]; const sentPeersArr = await promiseAllMaybeAsync(publishPromises); - if (isForkPostFulu(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 @@ -632,6 +638,143 @@ 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}`); + } + + // 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 !== slot) { + throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`); + } + + const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; + let dataColumnSidecars: gloas.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 === 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( + 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 validator key + // 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) + + // 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, + builderIndex: envelope.builderIndex, + isSelfBuild, + dataColumns: dataColumnSidecars.length, + }; + + // 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); + } + + 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 + const publishPromises = [ + // Gossip the signed execution payload envelope first + () => network.publishSignedExecutionPayloadEnvelope(signedExecutionPayloadEnvelope), + // For self-builds, publish all data column sidecars + ...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, 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)) { + // 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) { + chain.emitter.emit(routes.events.EventType.dataColumnSidecar, { + blockRoot: blockRootHex, + slot, + index: dataColumnSidecar.index, + kzgCommitments, + }); + } + } + } + + 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 18c52450c17e..c2d759c73d78 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"; @@ -69,7 +72,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"; @@ -901,6 +904,77 @@ 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); + + // 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, {}) + ); + const commonBlockBodyPromise = chain.produceCommonBlockBody({ + slot, + parentBlock, + randaoReveal, + graffiti: graffitiBytes, + }); + + 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}); + metrics?.blockProductionNumAggregated.observe({source}, block.body.attestations.length); + metrics?.blockProductionConsensusBlockValue.observe({source}, Number(formatWeiToEth(consensusBlockValue))); + metrics?.blockProductionExecutionPayloadValue.observe({source}, Number(formatWeiToEth(executionPayloadValue))); + + 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 { + timer?.({source}); + } + }, + async produceAttestationData({committeeIndex, slot}) { notWhileSyncing(); @@ -1532,5 +1606,54 @@ export function getValidatorApi( count: filteredRegistrations.length, }); }, + + async getExecutionPayloadEnvelope({slot, beaconBlockRoot}) { + const fork = config.getForkName(slot); + + if (!isForkPostGloas(fork)) { + throw new ApiError(400, `getExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`); + } + + notWhileSyncing(); + await waitForSlot(slot); + + const blockRootHex = toRootHex(beaconBlockRoot); + const produceResult = chain.blockProductionCache.get(blockRootHex); + + 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"); + } + + const {executionPayload, executionRequests, envelopeStateRoot} = produceResult as ProduceFullGloas; + + const envelope: gloas.ExecutionPayloadEnvelope = { + payload: executionPayload, + executionRequests: executionRequests, + builderIndex: BUILDER_INDEX_SELF_BUILD, + beaconBlockRoot, + slot, + stateRoot: envelopeStateRoot, + }; + + logger.info("Produced execution payload envelope", { + slot, + blockRoot: blockRootHex, + transactions: executionPayload.transactions.length, + blockHash: toRootHex(executionPayload.blockHash), + }); + + return { + data: envelope, + meta: { + version: fork, + }, + }; + }, }; } diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 9c10aab69fc8..7d4fa04f4004 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,14 +958,33 @@ 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); + 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, + 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; + } + // 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/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/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; 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..55c44501db74 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,34 @@ 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, + }; + + const processEnvelopeTimer = metrics?.blockPayload.executionPayloadEnvelopeProcessingTime.startTimer(); + processExecutionPayloadEnvelope(postBlockState, signedEnvelope, false); + processEnvelopeTimer?.(); + + 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 b386065231e0..76deccf25be0 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 {IForkChoice, 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,7 @@ import { deneb, electra, fulu, + gloas, } from "@lodestar/types"; import {Logger, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {ZERO_HASH_HEX} from "../../constants/index.js"; @@ -99,6 +104,20 @@ 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; + blobsBundle: BlobsBundle; + cells: fulu.Cell[][]; + /** + * Cached envelope state root computed during block production. + * This is the state root after running `processExecutionPayloadEnvelope` on the + * post-block state, and later used to construct the `ExecutionPayloadEnvelope`. + */ + envelopeStateRoot: Root; +}; export type ProduceFullFulu = { type: BlockType.Full; fork: ForkPostFulu; @@ -131,6 +150,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 +200,112 @@ export async function produceBlockBody( this.logger.verbose("Producing beacon block body", logMeta); if (isForkPostGloas(fork)) { - // TODO GLOAS: Set body.signedExecutionPayloadBid and body.payloadAttestation + // 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; + const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex); + + const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer(); + + this.logger.verbose("Preparing execution payload from engine", { + slot: blockSlot, + parentBlockRoot: toRootHex(parentBlockRoot), + feeRecipient, + }); + + // Get execution payload from EL + const prepareRes = await prepareExecutionPayload( + this, + this.logger, + fork, + 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 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}`); + } + + 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 + 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, + blobKzgCommitments: blobsBundle.commitments, + }; + const signedBid: gloas.SignedExecutionPayloadBid = { + message: bid, + signature: G2_POINT_AT_INFINITY, + }; + 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 + gloasBody.payloadAttestations = []; + blockBody = gloasBody as AssembledBodyType; + + // Store execution payload data required to construct execution payload envelope later + const gloasResult = produceResult as ProduceFullGloas; + gloasResult.executionPayload = executionPayload as ExecutionPayload; + gloasResult.executionRequests = executionRequests; + gloasResult.blobsBundle = blobsBundle; + 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 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; @@ -480,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); @@ -568,25 +690,46 @@ export function getPayloadAttributesForSSE( fork: ForkPostBellatrix, chain: { config: ChainForkConfig; + forkChoice: IForkChoice; }, { prepareState, 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, 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, - parentBlockNumber: prepareState.latestExecutionPayloadHeader.blockNumber, + parentBlockNumber, parentBlockRoot, parentBlockHash: parentHash, payloadAttributes, @@ -605,7 +748,7 @@ function preparePayloadAttributes( parentBlockRoot, feeRecipient, }: { - prepareState: CachedBeaconStateExecutions; + prepareState: CachedBeaconStateExecutions | CachedBeaconStateGloas; prepareSlot: Slot; parentBlockRoot: Root; feeRecipient: string; 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/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", 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/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 347f94f04156..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, @@ -31,6 +32,7 @@ import { capella, deneb, fulu, + gloas, phase0, } from "@lodestar/types"; import {BlockInputSource} from "../chain/blocks/blockInput/types.js"; @@ -86,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; @@ -95,6 +97,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 94e5b000909c..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, @@ -24,6 +25,7 @@ import { capella, deneb, fulu, + gloas, phase0, } from "@lodestar/types"; import {prettyPrintIndices, sleep} from "@lodestar/utils"; @@ -33,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"; @@ -354,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); @@ -489,6 +494,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], @@ -765,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..e196c90cc8ea 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; @@ -821,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 ({ diff --git a/packages/beacon-node/src/util/dataColumns.ts b/packages/beacon-node/src/util/dataColumns.ts index 8ad49d7142b7..11748bfc74a1 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. @@ -359,6 +367,39 @@ export function getDataColumnSidecarsFromColumnSidecar( ); } +/** + * In Gloas, data column sidecars have a simplified structure with `slot` and `beaconBlockRoot` + * instead of `signedBlockHeader`, `kzgCommitments`, and `kzgCommitmentsInclusionProof`. + */ +export function getDataColumnSidecarsForGloas( + slot: Slot, + beaconBlockRoot: Root, + cellsAndKzgProofs: {cells: Uint8Array[]; proofs: Uint8Array[]}[] +): gloas.DataColumnSidecars { + // No need to create data column sidecars if there are no blobs + if (cellsAndKzgProofs.length === 0) { + return []; + } + + 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; +} + /** * If we receive more than half of NUMBER_OF_COLUMNS (64) we should recover all remaining columns */ 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 69923a71ad99..a3f198273d5e 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, @@ -12,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"; @@ -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,104 @@ export class BlockProposingService { } } + /** + * 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 + * 4. Sign and publish the envelope + */ + 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 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, + }) + .catch((e: Error) => { + this.metrics?.blockProposingErrors.inc({error: "produce"}); + throw extendError(e, "Failed to produce block"); + }); + 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 block", { + ...debugLogCtx, + consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue), + blockRoot: blockRootHex, + }); + 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; + // 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({ + signedBlockContents: {signedBlock}, + broadcastValidation, + }) + .catch((e: Error) => { + this.metrics?.blockProposingErrors.inc({error: "publish"}); + throw extendError(e, "Failed to publish block"); + }) + ).assertOk(); + + this.logger.debug("Published beacon block", {...debugLogCtx, broadcastValidation}); + + // Step 3: Get the execution payload envelope + const envelopeRes = await this.api.validator.getExecutionPayloadEnvelope({ + slot, + beaconBlockRoot, + }); + const envelope = envelopeRes.value(); + const stateRootHex = toRootHex(envelope.stateRoot); + + 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); + + ( + 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 block and execution payload envelope", { + ...logCtx, + graffiti, + consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue), + blockRoot: blockRootHex, + }); + } + 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..1c461af9d36d 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"; @@ -487,6 +489,38 @@ export class ValidatorStore { } as SignedBeaconBlock | SignedBlindedBeaconBlock; } + async signExecutionPayloadEnvelope( + pubkey: BLSPubkey, + envelope: gloas.ExecutionPayloadEnvelope, + 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}`); + } + + 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, + }; + + 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); 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)}; } }