Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 145 additions & 29 deletions packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,20 @@ export type Endpoints = {
ProduceBlockV4Meta
>;

/** Produce a post-Gloas block with best-effort inclusion of a supplied signed bid. */
produceBlockV4WithBid: Endpoint<
"POST",
Omit<Endpoints["produceBlockV4"]["args"], "builderConfig"> & {
signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid;
builderBoostFactor: UintBn64;
},
Omit<Endpoints["produceBlockV4"]["request"], "query"> & {
query: Endpoints["produceBlockV4"]["request"]["query"] & {builder_boost_factor: string};
},
Endpoints["produceBlockV4"]["return"],
ProduceBlockV4Meta
>;

/**
* Get execution payload envelope.
* Retrieves the cached execution payload envelope for a given slot and beacon block root,
Expand Down Expand Up @@ -776,6 +790,38 @@ export type Endpoints = {
};

export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoints> {
const produceBlockV4Response = {
data: WithMeta(
({version, executionPayloadIncluded}) =>
(executionPayloadIncluded
? getPostGloasForkTypes(version).BlockContents
: getPostGloasForkTypes(version).BeaconBlock) as Type<
BeaconBlock<ForkPostGloas> | BlockContents<ForkPostGloas>
>
),
meta: {
toJson: (meta) => ProduceBlockV4MetaType.toJson(meta),
fromJson: (val, headers) => ({
...ProduceBlockV4MetaType.fromJson(val),
builderUrl: headers?.get(MetaHeader.BuilderUrl) ?? undefined,
}),
toHeadersObject: (meta) => ({
[MetaHeader.Version]: meta.version,
[MetaHeader.ConsensusBlockValue]: meta.consensusBlockValue.toString(),
[MetaHeader.ExecutionPayloadValue]: meta.executionPayloadValue.toString(),
[MetaHeader.ExecutionPayloadIncluded]: meta.executionPayloadIncluded.toString(),
...(meta.builderUrl !== undefined ? {[MetaHeader.BuilderUrl]: meta.builderUrl} : {}),
}),
fromHeaders: (headers) => ({
version: toForkName(headers.getRequired(MetaHeader.Version)),
consensusBlockValue: BigInt(headers.getRequired(MetaHeader.ConsensusBlockValue)),
executionPayloadValue: BigInt(headers.getRequired(MetaHeader.ExecutionPayloadValue)),
executionPayloadIncluded: toBoolean(headers.getRequired(MetaHeader.ExecutionPayloadIncluded)),
builderUrl: headers.get(MetaHeader.BuilderUrl) ?? undefined,
}),
},
} satisfies RouteDefinitions<Endpoints>["produceBlockV4"]["resp"];

return {
getAttesterDuties: {
url: "/eth/v1/validator/duties/attester/{epoch}",
Expand Down Expand Up @@ -1084,37 +1130,107 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
init: {
requestWireFormat: WireFormat.ssz,
},
resp: {
data: WithMeta(
({version, executionPayloadIncluded}) =>
(executionPayloadIncluded
? getPostGloasForkTypes(version).BlockContents
: getPostGloasForkTypes(version).BeaconBlock) as Type<
BeaconBlock<ForkPostGloas> | BlockContents<ForkPostGloas>
>
),
meta: {
toJson: (meta) => ProduceBlockV4MetaType.toJson(meta),
fromJson: (val, headers) => ({
...ProduceBlockV4MetaType.fromJson(val),
builderUrl: headers?.get(MetaHeader.BuilderUrl) ?? undefined,
}),
toHeadersObject: (meta) => ({
[MetaHeader.Version]: meta.version,
[MetaHeader.ConsensusBlockValue]: meta.consensusBlockValue.toString(),
[MetaHeader.ExecutionPayloadValue]: meta.executionPayloadValue.toString(),
[MetaHeader.ExecutionPayloadIncluded]: meta.executionPayloadIncluded.toString(),
...(meta.builderUrl !== undefined ? {[MetaHeader.BuilderUrl]: meta.builderUrl} : {}),
}),
fromHeaders: (headers) => ({
version: toForkName(headers.getRequired(MetaHeader.Version)),
consensusBlockValue: BigInt(headers.getRequired(MetaHeader.ConsensusBlockValue)),
executionPayloadValue: BigInt(headers.getRequired(MetaHeader.ExecutionPayloadValue)),
executionPayloadIncluded: toBoolean(headers.getRequired(MetaHeader.ExecutionPayloadIncluded)),
builderUrl: headers.get(MetaHeader.BuilderUrl) ?? undefined,
}),
resp: produceBlockV4Response,
},
produceBlockV4WithBid: {
url: "/eth/v4/validator/blocks/{slot}/with_bid",
method: "POST",
req: {
writeReqJson: ({
slot,
randaoReveal,
graffiti,
skipRandaoVerification,
feeRecipient,
strictFeeRecipientCheck,
includePayload,
signedExecutionPayloadBid,
builderBoostFactor,
}) => ({
params: {slot},
query: {
randao_reveal: toHex(randaoReveal),
graffiti: toGraffitiHex(graffiti),
skip_randao_verification: writeSkipRandaoVerification(skipRandaoVerification),
fee_recipient: feeRecipient,
strict_fee_recipient_check: strictFeeRecipientCheck,
include_payload: includePayload,
builder_boost_factor: builderBoostFactor.toString(),
},
body: ssz.gloas.SignedExecutionPayloadBid.toJson(signedExecutionPayloadBid),
headers: {[MetaHeader.Version]: config.getForkName(slot)},
}),
parseReqJson: ({params, query, body, headers}) => {
toForkName(fromHeaders(headers, MetaHeader.Version));
return {
slot: params.slot,
randaoReveal: fromHex(query.randao_reveal),
graffiti: fromGraffitiHex(query.graffiti),
skipRandaoVerification: parseSkipRandaoVerification(query.skip_randao_verification),
feeRecipient: query.fee_recipient,
strictFeeRecipientCheck: query.strict_fee_recipient_check,
includePayload: query.include_payload,
signedExecutionPayloadBid: ssz.gloas.SignedExecutionPayloadBid.fromJson(body),
builderBoostFactor: BigInt(query.builder_boost_factor),
};
},
writeReqSsz: ({
slot,
randaoReveal,
graffiti,
skipRandaoVerification,
feeRecipient,
strictFeeRecipientCheck,
includePayload,
signedExecutionPayloadBid,
builderBoostFactor,
}) => ({
params: {slot},
query: {
randao_reveal: toHex(randaoReveal),
graffiti: toGraffitiHex(graffiti),
skip_randao_verification: writeSkipRandaoVerification(skipRandaoVerification),
fee_recipient: feeRecipient,
strict_fee_recipient_check: strictFeeRecipientCheck,
include_payload: includePayload,
builder_boost_factor: builderBoostFactor.toString(),
},
body: ssz.gloas.SignedExecutionPayloadBid.serialize(signedExecutionPayloadBid),
headers: {[MetaHeader.Version]: config.getForkName(slot)},
}),
parseReqSsz: ({params, query, body, headers}) => {
toForkName(fromHeaders(headers, MetaHeader.Version));
return {
slot: params.slot,
randaoReveal: fromHex(query.randao_reveal),
graffiti: fromGraffitiHex(query.graffiti),
skipRandaoVerification: parseSkipRandaoVerification(query.skip_randao_verification),
feeRecipient: query.fee_recipient,
strictFeeRecipientCheck: query.strict_fee_recipient_check,
includePayload: query.include_payload,
signedExecutionPayloadBid: ssz.gloas.SignedExecutionPayloadBid.deserialize(body),
builderBoostFactor: BigInt(query.builder_boost_factor),
};
},
schema: {
params: {slot: Schema.UintRequired},
query: {
randao_reveal: Schema.StringRequired,
graffiti: Schema.String,
skip_randao_verification: Schema.String,
fee_recipient: Schema.String,
strict_fee_recipient_check: Schema.Boolean,
include_payload: Schema.BooleanRequired,
builder_boost_factor: Schema.StringRequired,
},
body: Schema.Object,
headers: {[MetaHeader.Version]: Schema.StringRequired},
},
},
init: {
requestWireFormat: WireFormat.ssz,
},
resp: produceBlockV4Response,
},
getExecutionPayloadEnvelope: {
url: "/eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}",
Expand Down
2 changes: 2 additions & 0 deletions packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const ignoredOperations = [
// TODO: remove once the pinned beacon-APIs spec version includes beacon-APIs#630
// (GET -> POST with a required BuilderConfig request body)
"produceBlockV4",
// TODO: remove once the pinned beacon-APIs spec includes beacon-APIs#627
"produceBlockV4WithBid",
// TODO: remove once the pinned beacon-APIs spec version includes beacon-APIs#630
"submitBuilderPreferences",
];
Expand Down
87 changes: 87 additions & 0 deletions packages/api/test/unit/beacon/produceBlockV4WithBid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {FastifyInstance} from "fastify";
import {afterEach, beforeEach, describe, expect, it} from "vitest";
import {config} from "@lodestar/config/default";
import {ssz} from "@lodestar/types";
import {Endpoints, getDefinitions} from "../../../src/beacon/routes/validator.js";
import {getRoutes} from "../../../src/beacon/server/validator.js";
import {MetaHeader} from "../../../src/utils/metadata.js";
import {getMockApi, getTestServer} from "../../utils/utils.js";
import {testData} from "./testData/validator.js";

describe("produceBlockV4WithBid", () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test is kinda useless, this should be covered by spec tests already

let server: FastifyInstance;
const api = getMockApi<Endpoints>(testData);
const definition = getDefinitions(config).produceBlockV4WithBid;

beforeEach(() => {
server = getTestServer().server;
server.route(getRoutes(config, api).produceBlockV4WithBid);
});

afterEach(async () => {
await server.close();
});

for (const format of ["json", "ssz"] as const) {
describe(format, () => {
function request() {
const args = {...testData.produceBlockV4WithBid.args, includePayload: true};
const req = format === "json" ? definition.req.writeReqJson(args) : definition.req.writeReqSsz(args);
return {
method: "POST" as const,
url: `/eth/v4/validator/blocks/${args.slot}/with_bid`,
query: Object.fromEntries(
Object.entries(req.query)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)])
),
headers: {
...req.headers,
"content-type": format === "json" ? "application/json" : "application/octet-stream",
accept: format === "json" ? "application/json" : "application/octet-stream",
},
payload: format === "json" ? JSON.stringify(req.body) : Buffer.from(req.body as Uint8Array),
};
}

it("returns full block contents for a local payload", async () => {
const data = ssz.gloas.BlockContents.defaultValue();
api.produceBlockV4WithBid.mockResolvedValue({
data,
meta: {...testData.produceBlockV4WithBid.res.meta, executionPayloadIncluded: true},
});
const res = await server.inject(request());
expect(res.statusCode).toBe(200);
expect(res.headers[MetaHeader.ExecutionPayloadIncluded.toLowerCase()]).toBe("true");
expect(
format === "json"
? ssz.gloas.BlockContents.fromJson(res.json().data)
: ssz.gloas.BlockContents.deserialize(new Uint8Array(res.rawPayload))
).toEqual(data);
});

it.each(["builder_boost_factor", "include_payload", "randao_reveal"])("requires %s", async (key) => {
const req = request();
delete req.query[key];
const res = await server.inject(req);
expect(res.statusCode).toBe(400);
expect(api.produceBlockV4WithBid).not.toHaveBeenCalled();
});

it("requires the consensus version header", async () => {
const req = request();
const headers: Record<string, string> = {...req.headers};
delete headers[MetaHeader.Version];
const res = await server.inject({...req, headers});
expect(res.statusCode).toBe(400);
expect(api.produceBlockV4WithBid).not.toHaveBeenCalled();
});

it("rejects malformed bid bodies", async () => {
const res = await server.inject({...request(), payload: format === "json" ? "{}" : Buffer.alloc(1)});
expect(res.statusCode).toBe(400);
expect(api.produceBlockV4WithBid).not.toHaveBeenCalled();
});
});
}
});
22 changes: 22 additions & 0 deletions packages/api/test/unit/beacon/testData/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,28 @@ export const testData: GenericServerTestCases<Endpoints> = {
},
},
},
produceBlockV4WithBid: {
args: {
slot: 32000,
randaoReveal,
graffiti,
skipRandaoVerification: true,
feeRecipient,
strictFeeRecipientCheck: true,
includePayload: false,
builderBoostFactor: 2n ** 64n - 1n,
signedExecutionPayloadBid: ssz.gloas.SignedExecutionPayloadBid.defaultValue(),
},
res: {
data: ssz.gloas.BeaconBlock.defaultValue(),
meta: {
version: ForkName.gloas,
consensusBlockValue: 1n,
executionPayloadValue: 2n,
executionPayloadIncluded: false,
},
},
},
getExecutionPayloadEnvelope: {
args: {slot: 32000, beaconBlockRoot: ZERO_HASH},
res: {
Expand Down
Loading
Loading