diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index fe26c559661c..ecb0b61928e2 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1517,7 +1517,7 @@ export function getValidatorApi( ); }); - await chain.executionBuilder.registerValidator(filteredRegistrations); + await chain.executionBuilder.registerValidator(currentEpoch, filteredRegistrations); logger.debug("Forwarded validator registrations to connected builder", { epoch: currentEpoch, diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index d7b36e06abc4..1cf87b947beb 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -32,11 +32,17 @@ import { ssz, sszTypesFor, } from "@lodestar/types"; -import {Logger, sleep, toHex, toRootHex} from "@lodestar/utils"; +import {Logger, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; import {IEth1ForBlockProduction} from "../../eth1/index.js"; import {numToQuantity} from "../../eth1/provider/utils.js"; -import {IExecutionBuilder, IExecutionEngine, PayloadAttributes, PayloadId} from "../../execution/index.js"; +import { + IExecutionBuilder, + IExecutionEngine, + PayloadAttributes, + PayloadId, + getExpectedGasLimit, +} from "../../execution/index.js"; import {fromGraffitiBuffer} from "../../util/graffiti.js"; import type {BeaconChain} from "../chain.js"; import {CommonBlockBody} from "../interface.js"; @@ -223,6 +229,22 @@ export async function produceBlockBody( fetchedTime, }); + const targetGasLimit = this.executionBuilder.getValidatorRegistration(proposerPubKey)?.gasLimit; + if (!targetGasLimit) { + this.logger.warn("Failed to get validator registration, could not check header gas limit", { + slot: blockSlot, + proposerIndex, + proposerPubKey: toPubkeyHex(proposerPubKey), + }); + } else { + const parentGasLimit = (currentState as CachedBeaconStateBellatrix).latestExecutionPayloadHeader.gasLimit; + const expectedGasLimit = getExpectedGasLimit(parentGasLimit, targetGasLimit); + + if (builderRes.header.gasLimit !== expectedGasLimit) { + throw Error(`Incorrect header gas limit ${builderRes.header.gasLimit} != ${expectedGasLimit}`); + } + } + if (ForkSeq[fork] >= ForkSeq.deneb) { const {blobKzgCommitments} = builderRes; if (blobKzgCommitments === undefined) { diff --git a/packages/beacon-node/src/execution/builder/cache.ts b/packages/beacon-node/src/execution/builder/cache.ts new file mode 100644 index 000000000000..9ca73efc56d6 --- /dev/null +++ b/packages/beacon-node/src/execution/builder/cache.ts @@ -0,0 +1,34 @@ +import {BLSPubkey, Epoch, bellatrix} from "@lodestar/types"; +import {toPubkeyHex} from "@lodestar/utils"; + +const REGISTRATION_PRESERVE_EPOCHS = 2; + +export type ValidatorRegistration = { + epoch: Epoch; + /** Preferred gas limit of validator */ + gasLimit: number; +}; + +export class ValidatorRegistrationCache { + private readonly registrationByValidatorPubkey: Map; + constructor() { + this.registrationByValidatorPubkey = new Map(); + } + + add(epoch: Epoch, {pubkey, gasLimit}: bellatrix.ValidatorRegistrationV1): void { + this.registrationByValidatorPubkey.set(toPubkeyHex(pubkey), {epoch, gasLimit}); + } + + prune(epoch: Epoch): void { + for (const [pubkeyHex, registration] of this.registrationByValidatorPubkey.entries()) { + // We only retain an registrations for REGISTRATION_PRESERVE_EPOCHS epochs + if (registration.epoch + REGISTRATION_PRESERVE_EPOCHS < epoch) { + this.registrationByValidatorPubkey.delete(pubkeyHex); + } + } + } + + get(pubkey: BLSPubkey): ValidatorRegistration | undefined { + return this.registrationByValidatorPubkey.get(toPubkeyHex(pubkey)); + } +} diff --git a/packages/beacon-node/src/execution/builder/http.ts b/packages/beacon-node/src/execution/builder/http.ts index 12e45412bafc..4d04a4b6f40e 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -6,6 +6,7 @@ import {ForkExecution, SLOTS_PER_EPOCH} from "@lodestar/params"; import {parseExecutionPayloadAndBlobsBundle, reconstructFullBlockOrContents} from "@lodestar/state-transition"; import { BLSPubkey, + Epoch, ExecutionPayloadHeader, Root, SignedBeaconBlockOrContents, @@ -19,6 +20,7 @@ import { } from "@lodestar/types"; import {toPrintableUrl} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; +import {ValidatorRegistration, ValidatorRegistrationCache} from "./cache.js"; import {IExecutionBuilder} from "./interface.js"; export type ExecutionBuilderHttpOpts = { @@ -60,6 +62,7 @@ const BUILDER_PROPOSAL_DELAY_TOLERANCE = 1000; export class ExecutionBuilderHttp implements IExecutionBuilder { readonly api: BuilderApi; readonly config: ChainForkConfig; + readonly cache: ValidatorRegistrationCache; readonly issueLocalFcUWithFeeRecipient?: string; // Builder needs to be explicity enabled using updateStatus status = false; @@ -93,6 +96,7 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { ); logger?.info("External builder", {url: toPrintableUrl(baseUrl)}); this.config = config; + this.cache = new ValidatorRegistrationCache(); this.issueLocalFcUWithFeeRecipient = opts.issueLocalFcUWithFeeRecipient; /** @@ -128,8 +132,17 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { } } - async registerValidator(registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise { + async registerValidator(epoch: Epoch, registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise { (await this.api.registerValidator({registrations})).assertOk(); + + for (const registration of registrations) { + this.cache.add(epoch, registration.message); + } + this.cache.prune(epoch); + } + + getValidatorRegistration(pubkey: BLSPubkey): ValidatorRegistration | undefined { + return this.cache.get(pubkey); } async getHeader( diff --git a/packages/beacon-node/src/execution/builder/index.ts b/packages/beacon-node/src/execution/builder/index.ts index fff66a3e8bc5..2add94394ca0 100644 --- a/packages/beacon-node/src/execution/builder/index.ts +++ b/packages/beacon-node/src/execution/builder/index.ts @@ -2,10 +2,11 @@ import {ChainForkConfig} from "@lodestar/config"; import {Logger} from "@lodestar/logger"; import {Metrics} from "../../metrics/metrics.js"; import {IExecutionBuilder} from "./interface.js"; +import {getExpectedGasLimit} from "./utils.js"; import {ExecutionBuilderHttp, ExecutionBuilderHttpOpts, defaultExecutionBuilderHttpOpts} from "./http.js"; -export {ExecutionBuilderHttp, defaultExecutionBuilderHttpOpts}; +export {ExecutionBuilderHttp, defaultExecutionBuilderHttpOpts, getExpectedGasLimit}; export type ExecutionBuilderOpts = {mode?: "http"} & ExecutionBuilderHttpOpts; export const defaultExecutionBuilderOpts: ExecutionBuilderOpts = defaultExecutionBuilderHttpOpts; diff --git a/packages/beacon-node/src/execution/builder/interface.ts b/packages/beacon-node/src/execution/builder/interface.ts index 06cdc1da4ed0..f326ec4bc451 100644 --- a/packages/beacon-node/src/execution/builder/interface.ts +++ b/packages/beacon-node/src/execution/builder/interface.ts @@ -1,6 +1,7 @@ import {ForkExecution} from "@lodestar/params"; import { BLSPubkey, + Epoch, ExecutionPayloadHeader, Root, SignedBeaconBlockOrContents, @@ -12,6 +13,7 @@ import { deneb, electra, } from "@lodestar/types"; +import {ValidatorRegistration} from "./cache.js"; export interface IExecutionBuilder { /** @@ -28,7 +30,8 @@ export interface IExecutionBuilder { updateStatus(shouldEnable: boolean): void; checkStatus(): Promise; - registerValidator(registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise; + registerValidator(epoch: Epoch, registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise; + getValidatorRegistration(pubkey: BLSPubkey): ValidatorRegistration | undefined; getHeader( fork: ForkExecution, slot: Slot, diff --git a/packages/beacon-node/src/execution/builder/utils.ts b/packages/beacon-node/src/execution/builder/utils.ts new file mode 100644 index 000000000000..42bbaece53e8 --- /dev/null +++ b/packages/beacon-node/src/execution/builder/utils.ts @@ -0,0 +1,14 @@ +/** + * Calculates expected gas limit based on parent gas limit and target gas limit + */ +export function getExpectedGasLimit(parentGasLimit: number, targetGasLimit: number): number { + const maxGasLimitDifference = Math.max(Math.floor(parentGasLimit / 1024) - 1, 0); + + if (targetGasLimit > parentGasLimit) { + const gasDiff = targetGasLimit - parentGasLimit; + return parentGasLimit + Math.min(gasDiff, maxGasLimitDifference); + } + + const gasDiff = parentGasLimit - targetGasLimit; + return parentGasLimit - Math.min(gasDiff, maxGasLimitDifference); +} diff --git a/packages/beacon-node/test/unit/execution/builder/cache.test.ts b/packages/beacon-node/test/unit/execution/builder/cache.test.ts new file mode 100644 index 000000000000..60667caf0a50 --- /dev/null +++ b/packages/beacon-node/test/unit/execution/builder/cache.test.ts @@ -0,0 +1,58 @@ +import {ssz} from "@lodestar/types"; +import {beforeEach, describe, expect, it} from "vitest"; +import {ValidatorRegistrationCache} from "../../../../src/execution/builder/cache.js"; + +describe("ValidatorRegistrationCache", () => { + const gasLimit1 = 30000000; + const gasLimit2 = 36000000; + + const validatorPubkey1 = new Uint8Array(48).fill(1); + const validatorPubkey2 = new Uint8Array(48).fill(2); + + const validatorRegistration1 = ssz.bellatrix.ValidatorRegistrationV1.defaultValue(); + validatorRegistration1.pubkey = validatorPubkey1; + validatorRegistration1.gasLimit = gasLimit1; + + const validatorRegistration2 = ssz.bellatrix.ValidatorRegistrationV1.defaultValue(); + validatorRegistration2.pubkey = validatorPubkey2; + validatorRegistration2.gasLimit = gasLimit2; + + let cache: ValidatorRegistrationCache; + + beforeEach(() => { + // max 2 items + cache = new ValidatorRegistrationCache(); + cache.add(1, validatorRegistration1); + cache.add(3, validatorRegistration2); + }); + + it("get for registered validator", () => { + expect(cache.get(validatorPubkey1)?.gasLimit).toBe(gasLimit1); + }); + + it("get for unknown validator", () => { + const unknownValidatorPubkey = new Uint8Array(48).fill(3); + expect(cache.get(unknownValidatorPubkey)).toBe(undefined); + }); + + it("override and get latest", () => { + const newGasLimit = 60000000; + const registration = ssz.bellatrix.ValidatorRegistrationV1.defaultValue(); + registration.pubkey = validatorPubkey1; + registration.gasLimit = newGasLimit; + + cache.add(5, registration); + + expect(cache.get(validatorPubkey1)?.gasLimit).toBe(newGasLimit); + }); + + it("prune", () => { + cache.prune(4); + + // No registration as it has been pruned + expect(cache.get(validatorPubkey1)).toBe(undefined); + + // Registration hasn't been pruned + expect(cache.get(validatorPubkey2)?.gasLimit).toBe(gasLimit2); + }); +}); diff --git a/packages/beacon-node/test/unit/execution/builder/utils.test.ts b/packages/beacon-node/test/unit/execution/builder/utils.test.ts new file mode 100644 index 000000000000..fcf2a7dc2c6d --- /dev/null +++ b/packages/beacon-node/test/unit/execution/builder/utils.test.ts @@ -0,0 +1,66 @@ +import {describe, expect, it} from "vitest"; +import {getExpectedGasLimit} from "../../../../src/execution/builder/utils.js"; + +describe("execution / builder / utils", () => { + describe("getExpectedGasLimit", () => { + const testCases: { + name: string; + parentGasLimit: number; + targetGasLimit: number; + expected: number; + }[] = [ + { + name: "Increase within limit", + parentGasLimit: 30000000, + targetGasLimit: 30000100, + expected: 30000100, + }, + { + name: "Increase exceeding limit", + parentGasLimit: 30000000, + targetGasLimit: 36000000, + expected: 30029295, // maxGasLimitDifference = (30000000 / 1024) - 1 = 29295 + }, + { + name: "Decrease within limit", + parentGasLimit: 30000000, + targetGasLimit: 29999990, + expected: 29999990, + }, + { + name: "Decrease exceeding limit", + parentGasLimit: 36000000, + targetGasLimit: 30000000, + expected: 35964845, // maxGasLimitDifference = (36000000 / 1024) - 1 = 35155 + }, + { + name: "Target equals parent", + parentGasLimit: 30000000, + targetGasLimit: 30000000, + expected: 30000000, // No change + }, + { + name: "Very small parent gas limit", + parentGasLimit: 1025, + targetGasLimit: 2000, + expected: 1025, + }, + { + name: "Target far below parent but limited", + parentGasLimit: 30000000, + targetGasLimit: 10000000, + expected: 29970705, // maxGasLimitDifference = (30000000 / 1024) - 1 = 29295 + }, + { + name: "Parent gas limit under flows", + parentGasLimit: 1023, + targetGasLimit: 30000000, + expected: 1023, + }, + ]; + + it.each(testCases)("$name", ({parentGasLimit, targetGasLimit, expected}) => { + expect(getExpectedGasLimit(parentGasLimit, targetGasLimit)).toBe(expected); + }); + }); +});