diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index c4199f505bb2..98b0035e2771 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -226,6 +226,20 @@ export type Endpoints = { EmptyMeta >; + /** + * Publish signed execution payload bid. + * Instructs the beacon node to broadcast a signed execution payload bid to the network, + * to be gossiped for potential inclusion in block building. A success response (20x) indicates + * that the bid passed gossip validation and was successfully broadcast onto the network. + */ + publishExecutionPayloadBid: Endpoint< + "POST", + {signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid}, + {body: unknown; headers: {[MetaHeader.Version]: string}}, + EmptyResponseData, + EmptyMeta + >; + /** * Get signed execution payload envelope. * Retrieves signed execution payload envelope for a given block id. @@ -646,6 +660,50 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { + const fork = config.getForkName(signedExecutionPayloadBid.message.slot); + return { + body: getPostGloasForkTypes(fork).SignedExecutionPayloadBid.toJson(signedExecutionPayloadBid), + headers: { + [MetaHeader.Version]: fork, + }, + }; + }, + parseReqJson: ({body, headers}) => { + const fork = toForkName(fromHeaders(headers, MetaHeader.Version)); + return { + signedExecutionPayloadBid: getPostGloasForkTypes(fork).SignedExecutionPayloadBid.fromJson(body), + }; + }, + writeReqSsz: ({signedExecutionPayloadBid}) => { + const fork = config.getForkName(signedExecutionPayloadBid.message.slot); + return { + body: getPostGloasForkTypes(fork).SignedExecutionPayloadBid.serialize(signedExecutionPayloadBid), + headers: { + [MetaHeader.Version]: fork, + }, + }; + }, + parseReqSsz: ({body, headers}) => { + const fork = toForkName(fromHeaders(headers, MetaHeader.Version)); + return { + signedExecutionPayloadBid: getPostGloasForkTypes(fork).SignedExecutionPayloadBid.deserialize(body), + }; + }, + schema: { + body: Schema.Object, + headers: {[MetaHeader.Version]: Schema.String}, + }, + }, + resp: EmptyResponseCodec, + init: { + requestWireFormat: WireFormat.ssz, + }, + }, getSignedExecutionPayloadEnvelope: { url: "/eth/v1/beacon/execution_payload_envelope/{block_id}", method: "GET", diff --git a/packages/api/test/unit/beacon/oapiSpec.test.ts b/packages/api/test/unit/beacon/oapiSpec.test.ts index e7bf00e854a3..62e9d4829158 100644 --- a/packages/api/test/unit/beacon/oapiSpec.test.ts +++ b/packages/api/test/unit/beacon/oapiSpec.test.ts @@ -56,7 +56,6 @@ const ignoredOperations = [ "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.1 - "publishExecutionPayloadBid", "getExecutionPayloadBid", "getSignedExecutionPayloadEnvelope", ]; diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 412eadd1a0dc..7fb5c4e956fd 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -95,6 +95,10 @@ export const testData: GenericServerTestCases = { args: {signedExecutionPayloadEnvelope: ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue()}, res: undefined, }, + publishExecutionPayloadBid: { + args: {signedExecutionPayloadBid: ssz.gloas.SignedExecutionPayloadBid.defaultValue()}, + res: undefined, + }, getSignedExecutionPayloadEnvelope: { args: {blockId: "head"}, res: { 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 5b6aca40d50a..8c4b0d0950b5 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -32,6 +32,7 @@ import { fulu, gloas, isDenebBlockContents, + ssz, sszTypesFor, } from "@lodestar/types"; import {fromHex, sleep, toHex, toRootHex} from "@lodestar/utils"; @@ -41,7 +42,14 @@ import {ImportBlockOpts} from "../../../../chain/blocks/types.js"; import {verifyBlocksInEpoch} from "../../../../chain/blocks/verifyBlock.js"; import {BeaconChain} from "../../../../chain/chain.js"; import {ChainEvent} from "../../../../chain/emitter.js"; -import {BlockError, BlockErrorCode, BlockGossipError} from "../../../../chain/errors/index.js"; +import { + BlockError, + BlockErrorCode, + BlockGossipError, + ExecutionPayloadBidError, + ExecutionPayloadBidErrorCode, + GossipAction, +} from "../../../../chain/errors/index.js"; import { BlockType, ProduceFullBellatrix, @@ -50,6 +58,7 @@ import { ProduceFullGloas, } from "../../../../chain/produceBlock/index.js"; import {validateGossipBlock} from "../../../../chain/validation/block.js"; +import {validateApiExecutionPayloadBid} from "../../../../chain/validation/executionPayloadBid.js"; import {validateApiExecutionPayloadEnvelope} from "../../../../chain/validation/executionPayloadEnvelope.js"; import {OpSource} from "../../../../chain/validatorMonitor.js"; import { @@ -825,6 +834,47 @@ export function getBeaconBlockApi({ }); }, + async publishExecutionPayloadBid({signedExecutionPayloadBid}) { + const fork = config.getForkName(signedExecutionPayloadBid.message.slot); + if (!isForkPostGloas(fork)) { + throw new ApiError(400, `publishExecutionPayloadBid not supported for pre-gloas fork=${fork}`); + } + + const logCtx = { + slot: signedExecutionPayloadBid.message.slot, + builderIndex: signedExecutionPayloadBid.message.builderIndex, + }; + + try { + const {proposerIndex} = await validateApiExecutionPayloadBid(chain, signedExecutionPayloadBid); + + const insertOutcome = chain.executionPayloadBidPool.add(signedExecutionPayloadBid.message); + metrics?.opPool.executionPayloadBidPool.apiInsertOutcome.inc({insertOutcome}); + + chain.validatorMonitor?.registerExecutionPayloadBid( + OpSource.api, + proposerIndex, + signedExecutionPayloadBid.message + ); + await network.publishExecutionPayloadBid(signedExecutionPayloadBid); + + chain.emitter.emit(routes.events.EventType.executionPayloadBid, { + version: fork, + data: signedExecutionPayloadBid, + }); + } catch (e) { + if (e instanceof ExecutionPayloadBidError && e.type.code === ExecutionPayloadBidErrorCode.BID_ALREADY_KNOWN) { + chain.logger.debug("Ignoring known execution payload bid", logCtx); + return; + } + chain.logger.verbose("Error on publishExecutionPayloadBid", logCtx, e as Error); + if (e instanceof ExecutionPayloadBidError && e.action === GossipAction.REJECT) { + chain.persistInvalidSszValue(ssz.gloas.SignedExecutionPayloadBid, signedExecutionPayloadBid, "api_reject"); + } + throw e; + } + }, + async getSignedExecutionPayloadEnvelope({blockId}, context) { const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId); const slot = block.message.slot; diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index 9794b40bfd98..d58815f3d6cc 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -115,6 +115,7 @@ export interface INetwork extends INetworkCorePublic { publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise; publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise; publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise; + publishExecutionPayloadBid(signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid): 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 1d184d210a65..2370a45f0eae 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -537,6 +537,17 @@ export class Network implements INetwork { ); } + async publishExecutionPayloadBid(signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid): Promise { + const epoch = computeEpochAtSlot(signedExecutionPayloadBid.message.slot); + const boundary = this.config.getForkBoundaryAtEpoch(epoch); + + return this.publishGossip( + {type: GossipType.execution_payload_bid, boundary}, + signedExecutionPayloadBid, + {ignoreDuplicatePublishError: true} + ); + } + private async publishGossip( topic: GossipTopicMap[K], object: GossipTypeMap[K],