diff --git a/packages/api/src/builder/routes.ts b/packages/api/src/builder/routes.ts index 018110e5ede..a203a93b8af 100644 --- a/packages/api/src/builder/routes.ts +++ b/packages/api/src/builder/routes.ts @@ -78,9 +78,6 @@ export type Endpoints = { >; }; -// NOTE: Builder API does not support SSZ as per spec, need to keep routes as JSON-only for now -// See https://github.com/ethereum/builder-specs/issues/53 for more details - export function getDefinitions(config: ChainForkConfig): RouteDefinitions { return { status: { @@ -125,7 +122,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { const fork = config.getForkName(signedBlindedBlock.message.slot); return { @@ -141,11 +138,26 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { + const fork = config.getForkName(signedBlindedBlock.message.slot); + return { + body: getExecutionForkTypes(fork).SignedBlindedBeaconBlock.serialize(signedBlindedBlock), + headers: { + [MetaHeader.Version]: fork, + }, + }; + }, + parseReqSsz: ({body, headers}) => { + const fork = toForkName(fromHeaders(headers, MetaHeader.Version)); + return { + signedBlindedBlock: getExecutionForkTypes(fork).SignedBlindedBeaconBlock.deserialize(body), + }; + }, schema: { body: Schema.Object, headers: {[MetaHeader.Version]: Schema.String}, }, - }), + }, resp: { data: WithVersion((fork: ForkName) => { return isForkBlobs(fork) diff --git a/packages/beacon-node/src/execution/builder/http.ts b/packages/beacon-node/src/execution/builder/http.ts index 87920e833f5..814fee72cd8 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -18,6 +18,7 @@ import {SLOTS_PER_EPOCH, ForkExecution} from "@lodestar/params"; import {toPrintableUrl} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {IExecutionBuilder} from "./interface.js"; +import {WireFormat} from "@lodestar/api"; export type ExecutionBuilderHttpOpts = { enabled: boolean; @@ -53,6 +54,13 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { faultInspectionWindow: number; allowedFaults: number; + /** + * Determine if SSZ is supported by requesting an SSZ encoded response in the `getHeader` request. + * If the builder responds with a SSZ serialized `SignedBuilderBid` it indicates support for submitting + * the `SignedBlindedBeaconBlock` as SSZ serialized bytes instead of JSON via `submitBlindedBlock`. + */ + private sszSupported = false; + constructor( opts: ExecutionBuilderHttpOpts, config: ChainForkConfig, @@ -123,14 +131,18 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { blobKzgCommitments?: deneb.BlobKzgCommitments; executionRequests?: electra.ExecutionRequests; }> { - const signedBuilderBid = ( - await this.api.getHeader({slot, parentHash, proposerPubkey}, {timeoutMs: BUILDER_PROPOSAL_DELAY_TOLERANCE}) - ).value(); + const res = await this.api.getHeader( + {slot, parentHash, proposerPubkey}, + {timeoutMs: BUILDER_PROPOSAL_DELAY_TOLERANCE} + ); + const signedBuilderBid = res.value(); if (!signedBuilderBid) { throw Error("No bid received"); } + this.sszSupported = res.wireFormat() === WireFormat.ssz; + const {header, value: executionPayloadValue} = signedBuilderBid.message; const {blobKzgCommitments} = signedBuilderBid.message as deneb.BuilderBid; const {executionRequests} = signedBuilderBid.message as electra.BuilderBid; @@ -138,9 +150,12 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { } async submitBlindedBlock(signedBlindedBlock: SignedBlindedBeaconBlock): Promise { - const data = (await this.api.submitBlindedBlock({signedBlindedBlock}, {retries: 2})).value(); + const res = await this.api.submitBlindedBlock( + {signedBlindedBlock}, + {retries: 2, requestWireFormat: this.sszSupported ? WireFormat.ssz : WireFormat.json} + ); - const {executionPayload, blobsBundle} = parseExecutionPayloadAndBlobsBundle(data); + const {executionPayload, blobsBundle} = parseExecutionPayloadAndBlobsBundle(res.value()); // for the sake of timely proposals we can skip matching the payload with payloadHeader // if the roots (transactions, withdrawals) don't match, this will likely lead to a block with