diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index 20203fa9fcb4..52c38993156d 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -44,6 +44,12 @@ export type BlockHeaderResponse = { header: phase0.SignedBeaconBlockHeader; }; +export enum BroadcastValidation { + gossip = "gossip", + consensus = "consensus", + consensusAndEquivocation = "consensus_and_equivocation", +} + export type Api = { /** * Get block @@ -167,6 +173,20 @@ export type Api = { HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE > >; + + publishBlockV2( + blockOrContents: allForks.SignedBeaconBlock | SignedBlockContents, + opts: {broadcastValidation: BroadcastValidation} + ): Promise< + ApiClientResponse< + { + [HttpStatusCode.OK]: void; + [HttpStatusCode.ACCEPTED]: void; + }, + HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE + > + >; + /** * Publish a signed blinded block by submitting it to the mev relay and patching in the block * transactions beacon node gets in response. @@ -180,6 +200,19 @@ export type Api = { HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE > >; + + publishBlindedBlockV2( + blindedBlockOrContents: allForks.SignedBlindedBeaconBlock | SignedBlindedBlockContents, + opts: {broadcastValidation: BroadcastValidation} + ): Promise< + ApiClientResponse< + { + [HttpStatusCode.OK]: void; + [HttpStatusCode.ACCEPTED]: void; + }, + HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE + > + >; /** * Get block BlobSidecar * Retrieves BlobSidecar included in requested block. @@ -204,7 +237,9 @@ export const routesData: RoutesData = { getBlockHeaders: {url: "/eth/v1/beacon/headers", method: "GET"}, getBlockRoot: {url: "/eth/v1/beacon/blocks/{block_id}/root", method: "GET"}, publishBlock: {url: "/eth/v1/beacon/blocks", method: "POST"}, + publishBlockV2: {url: "/eth/v2/beacon/blocks", method: "POST"}, publishBlindedBlock: {url: "/eth/v1/beacon/blinded_blocks", method: "POST"}, + publishBlindedBlockV2: {url: "/eth/v2/beacon/blinded_blocks", method: "POST"}, getBlobSidecars: {url: "/eth/v1/beacon/blob_sidecars/{block_id}", method: "GET"}, }; @@ -220,7 +255,9 @@ export type ReqTypes = { getBlockHeaders: {query: {slot?: number; parent_root?: string}}; getBlockRoot: BlockIdOnlyReq; publishBlock: {body: unknown}; + publishBlockV2: {body: unknown; query: {broadcast_validation: string}}; publishBlindedBlock: {body: unknown}; + publishBlindedBlockV2: {body: unknown; query: {broadcast_validation: string}}; getBlobSidecars: BlockIdOnlyReq; }; @@ -277,7 +314,35 @@ export function getReqSerializers(config: ChainForkConfig): ReqSerializers ({ + body: AllForksSignedBlockOrContents.toJson(item), + query: {broadcast_validation: broadcastValidation}, + }), + parseReq: ({body, query}) => [ + AllForksSignedBlockOrContents.fromJson(body), + {broadcastValidation: query.broadcast_validation as BroadcastValidation}, + ], + schema: { + body: Schema.Object, + query: {broadcast_validation: Schema.String}, + }, + }, publishBlindedBlock: reqOnlyBody(AllForksSignedBlindedBlockOrContents, Schema.Object), + publishBlindedBlockV2: { + writeReq: (item, {broadcastValidation}) => ({ + body: AllForksSignedBlindedBlockOrContents.toJson(item), + query: {broadcast_validation: broadcastValidation}, + }), + parseReq: ({body, query}) => [ + AllForksSignedBlindedBlockOrContents.fromJson(body), + {broadcastValidation: query.broadcast_validation as BroadcastValidation}, + ], + schema: { + body: Schema.Object, + query: {broadcast_validation: Schema.String}, + }, + }, getBlobSidecars: blockIdOnlyReq, }; } diff --git a/packages/api/src/beacon/routes/beacon/index.ts b/packages/api/src/beacon/routes/beacon/index.ts index 3146aa694d3e..d8879ca2d695 100644 --- a/packages/api/src/beacon/routes/beacon/index.ts +++ b/packages/api/src/beacon/routes/beacon/index.ts @@ -15,7 +15,7 @@ import * as state from "./state.js"; export * as block from "./block.js"; export * as pool from "./pool.js"; export * as state from "./state.js"; -export {BlockId, BlockHeaderResponse} from "./block.js"; +export {BlockId, BlockHeaderResponse, BroadcastValidation} from "./block.js"; export {AttestationFilters} from "./pool.js"; // TODO: Review if re-exporting all these types is necessary export { diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 2cca22564431..c63afc1e919c 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -52,10 +52,18 @@ export const testData: GenericServerTestCases = { args: [ssz.phase0.SignedBeaconBlock.defaultValue()], res: undefined, }, + publishBlockV2: { + args: [ssz.phase0.SignedBeaconBlock.defaultValue(), "consensus"], + res: undefined, + }, publishBlindedBlock: { args: [getDefaultBlindedBlock(64)], res: undefined, }, + publishBlindedBlockV2: { + args: [getDefaultBlindedBlock(64), "consensus"], + res: undefined, + }, getBlobSidecars: { args: ["head"], res: {executionOptimistic: true, data: ssz.deneb.BlobSidecars.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 663e316f413c..c28c35c901b9 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -19,6 +19,9 @@ import {NetworkEvent} from "../../../../network/index.js"; import {ApiModules} from "../../types.js"; import {resolveBlockId, toBeaconHeaderResponse} from "./utils.js"; +type PublishBlockOpts = ImportBlockOpts & {broadcastValidation?: routes.beacon.BroadcastValidation}; +const defaultPublishOpts = {broadcastValidation: routes.beacon.BroadcastValidation.consensus}; + /** * Validator clock may be advanced from beacon's clock. If the validator requests a resource in a * future slot, wait some time instead of rejecting the request because it's in the future @@ -189,7 +192,11 @@ export function getBeaconBlockApi({ }; }, - async publishBlindedBlock(signedBlindedBlockOrContents) { + async publishBlindedBlockV2(signedBlindedBlockOrContents, opts) { + await this.publishBlindedBlock(signedBlindedBlockOrContents, opts); + }, + + async publishBlindedBlock(signedBlindedBlockOrContents, opts: PublishBlockOpts = defaultPublishOpts) { const executionBuilder = chain.executionBuilder; if (!executionBuilder) throw Error("exeutionBuilder required to publish SignedBlindedBeaconBlock"); // Mechanism for blobs & blocks on builder is not yet finalized @@ -199,14 +206,23 @@ export function getBeaconBlockApi({ const signedBlockOrContents = await executionBuilder.submitBlindedBlock(signedBlindedBlockOrContents); // the full block is published by relay and it's possible that the block is already known to us by gossip // see https://github.com/ChainSafe/lodestar/issues/5404 - return this.publishBlock(signedBlockOrContents, {ignoreIfKnown: true}); + return this.publishBlock(signedBlockOrContents, {...opts, ignoreIfKnown: true}); } }, - async publishBlock(signedBlockOrContents, opts: ImportBlockOpts = {}) { + async publishBlockV2(signedBlockOrContents, opts: PublishBlockOpts = defaultPublishOpts) { + await this.publishBlock(signedBlockOrContents, opts); + }, + + async publishBlock(signedBlockOrContents, opts: PublishBlockOpts = defaultPublishOpts) { const seenTimestampSec = Date.now() / 1000; let blockForImport: BlockInput, signedBlock: allForks.SignedBeaconBlock, signedBlobs: deneb.SignedBlobSidecars; + const {broadcastValidation} = opts; + if (broadcastValidation !== routes.beacon.BroadcastValidation.consensus) { + throw Error("Broadcast Validation of consensus type accepted only."); + } + if (isSignedBlockContents(signedBlockOrContents)) { ({signedBlock, signedBlobSidecars: signedBlobs} = signedBlockOrContents); blockForImport = getBlockInput.postDeneb(