From ba7fe023c6224465311170e9e550cb882f839dba Mon Sep 17 00:00:00 2001 From: harkamal Date: Thu, 28 Apr 2022 16:44:54 +0530 Subject: [PATCH] Add and use a default fee recipient for a validator process --- packages/api/src/routes/validator.ts | 19 +++++++ packages/api/test/unit/validator.test.ts | 4 ++ packages/cli/src/cmds/validator/handler.ts | 8 ++- packages/cli/src/cmds/validator/options.ts | 11 ++++ .../options/beaconNodeOptions/execution.ts | 26 ++++++++- packages/cli/src/util/feeRecipient.ts | 16 ++++++ packages/cli/src/util/index.ts | 1 + .../cli/test/unit/validator/options.test.ts | 28 ++++++++++ .../lodestar/src/api/impl/validator/index.ts | 6 ++- .../lodestar/src/chain/factory/block/body.ts | 17 +++++- .../lodestar/src/chain/factory/block/index.ts | 9 ++-- .../lodestar/src/executionEngine/disabled.ts | 11 ++++ packages/lodestar/src/executionEngine/http.ts | 19 ++++++- .../lodestar/src/executionEngine/index.ts | 15 +++++- .../lodestar/src/executionEngine/interface.ts | 11 +++- packages/lodestar/src/executionEngine/mock.ts | 17 +++++- packages/lodestar/src/node/options.ts | 3 +- .../chain/factory/block/blockAssembly.test.ts | 1 - .../unit/chain/factory/block/body.test.ts | 2 +- .../lodestar/test/utils/node/validator.ts | 6 ++- .../src/services/prepareBeaconProposer.ts | 54 +++++++++++++++++++ packages/validator/src/validator.ts | 19 ++++++- 22 files changed, 278 insertions(+), 25 deletions(-) create mode 100644 packages/cli/src/util/feeRecipient.ts create mode 100644 packages/cli/test/unit/validator/options.test.ts create mode 100644 packages/validator/src/services/prepareBeaconProposer.ts diff --git a/packages/api/src/routes/validator.ts b/packages/api/src/routes/validator.ts index aaf97c8867ff..3e558c20fd20 100644 --- a/packages/api/src/routes/validator.ts +++ b/packages/api/src/routes/validator.ts @@ -13,6 +13,7 @@ import { ssz, UintNum64, ValidatorIndex, + ExecutionAddress, } from "@chainsafe/lodestar-types"; import { RoutesData, @@ -44,6 +45,11 @@ export type SyncCommitteeSubscription = { untilEpoch: Epoch; }; +export type ProposerPreparationData = { + validatorIndex: ValidatorIndex; + feeRecipient: ExecutionAddress; +}; + export type ProposerDuty = { slot: Slot; validatorIndex: ValidatorIndex; @@ -197,6 +203,8 @@ export type Api = { prepareBeaconCommitteeSubnet(subscriptions: BeaconCommitteeSubscription[]): Promise; prepareSyncCommitteeSubnets(subscriptions: SyncCommitteeSubscription[]): Promise; + + prepareBeaconProposer(proposers: ProposerPreparationData[]): Promise; }; /** @@ -215,6 +223,7 @@ export const routesData: RoutesData = { publishContributionAndProofs: {url: "/eth/v1/validator/contribution_and_proofs", method: "POST"}, prepareBeaconCommitteeSubnet: {url: "/eth/v1/validator/beacon_committee_subscriptions", method: "POST"}, prepareSyncCommitteeSubnets: {url: "/eth/v1/validator/sync_committee_subscriptions", method: "POST"}, + prepareBeaconProposer: {url: "/eth/v1/validator/prepare_beacon_proposer", method: "POST"}, }; /* eslint-disable @typescript-eslint/naming-convention */ @@ -231,6 +240,7 @@ export type ReqTypes = { publishContributionAndProofs: {body: unknown}; prepareBeaconCommitteeSubnet: {body: unknown}; prepareSyncCommitteeSubnets: {body: unknown}; + prepareBeaconProposer: {body: unknown}; }; export function getReqSerializers(): ReqSerializers { @@ -254,6 +264,14 @@ export function getReqSerializers(): ReqSerializers { {jsonCase: "eth2"} ); + const ProposerPreparationData = new ContainerType( + { + validatorIndex: ssz.ValidatorIndex, + feeRecipient: ssz.ExecutionAddress, + }, + {jsonCase: "eth2"} + ); + const produceBlock: ReqSerializers["produceBlock"] = { writeReq: (slot, randaoReveal, grafitti) => ({ params: {slot}, @@ -330,6 +348,7 @@ export function getReqSerializers(): ReqSerializers { publishContributionAndProofs: reqOnlyBody(ArrayOf(ssz.altair.SignedContributionAndProof), Schema.ObjectArray), prepareBeaconCommitteeSubnet: reqOnlyBody(ArrayOf(BeaconCommitteeSubscription), Schema.ObjectArray), prepareSyncCommitteeSubnets: reqOnlyBody(ArrayOf(SyncCommitteeSubscription), Schema.ObjectArray), + prepareBeaconProposer: reqOnlyBody(ArrayOf(ProposerPreparationData), Schema.ObjectArray), }; } diff --git a/packages/api/test/unit/validator.test.ts b/packages/api/test/unit/validator.test.ts index 532a90c19550..88a6578fd63b 100644 --- a/packages/api/test/unit/validator.test.ts +++ b/packages/api/test/unit/validator.test.ts @@ -74,6 +74,10 @@ describe("validator", () => { args: [[{validatorIndex: 1, syncCommitteeIndices: [2], untilEpoch: 3}]], res: undefined, }, + prepareBeaconProposer: { + args: [[{validatorIndex: 1, feeRecipient: new Uint8Array(20)}]], + res: undefined, + }, }); // TODO: Extra tests to implement maybe diff --git a/packages/cli/src/cmds/validator/handler.ts b/packages/cli/src/cmds/validator/handler.ts index 118e335e3c56..da3e759b177b 100644 --- a/packages/cli/src/cmds/validator/handler.ts +++ b/packages/cli/src/cmds/validator/handler.ts @@ -7,11 +7,11 @@ import {RegistryMetricCreator, collectNodeJSMetrics, HttpMetricsServer} from "@c import {getBeaconConfigFromArgs} from "../../config"; import {IGlobalArgs} from "../../options"; import {YargsError, getDefaultGraffiti, initBLS, mkdir, getCliLogger} from "../../util"; -import {onGracefulShutdown} from "../../util"; +import {onGracefulShutdown, parseFeeRecipient} from "../../util"; import {getVersionData} from "../../util/version"; import {getBeaconPaths} from "../beacon/paths"; import {getValidatorPaths} from "./paths"; -import {IValidatorCliArgs, validatorMetricsDefaultOptions} from "./options"; +import {IValidatorCliArgs, validatorMetricsDefaultOptions, defaultDefaultSuggestedFeeRecipient} from "./options"; import {getLocalSecretKeys, getExternalSigners, groupExternalSignersByUrl} from "./keys"; /** @@ -21,6 +21,9 @@ export async function validatorHandler(args: IValidatorCliArgs & IGlobalArgs): P await initBLS(); const graffiti = args.graffiti || getDefaultGraffiti(); + const defaultSuggestedFeeRecipient = parseFeeRecipient( + args.defaultSuggestedFeeRecipient ?? defaultDefaultSuggestedFeeRecipient + ); const validatorPaths = getValidatorPaths(args); const beaconPaths = getBeaconPaths(args); @@ -129,6 +132,7 @@ export async function validatorHandler(args: IValidatorCliArgs & IGlobalArgs): P signers, graffiti, afterBlockDelaySlotFraction: args.afterBlockDelaySlotFraction, + defaultSuggestedFeeRecipient, }, controller.signal, metrics diff --git a/packages/cli/src/cmds/validator/options.ts b/packages/cli/src/cmds/validator/options.ts index 9eec393c894e..06778f3e6a2e 100644 --- a/packages/cli/src/cmds/validator/options.ts +++ b/packages/cli/src/cmds/validator/options.ts @@ -4,7 +4,9 @@ import {accountValidatorOptions, IAccountValidatorArgs} from "../account/cmds/va import {logOptions, beaconPathsOptions} from "../beacon/options"; import {IBeaconPaths} from "../beacon/paths"; import {KeymanagerArgs, keymanagerOptions} from "../../options/keymanagerOptions"; +import {defaultDefaultSuggestedFeeRecipient} from "@chainsafe/lodestar"; +export {defaultDefaultSuggestedFeeRecipient}; export const validatorMetricsDefaultOptions = { enabled: false, port: 5064, @@ -19,6 +21,8 @@ export type IValidatorCliArgs = IAccountValidatorArgs & force: boolean; graffiti: string; afterBlockDelaySlotFraction?: number; + defaultSuggestedFeeRecipient?: string; + importKeystoresPath?: string[]; importKeystoresPassword?: string; externalSignerUrl?: string; @@ -68,6 +72,13 @@ export const validatorOptions: ICliCommandOptions = { type: "number", }, + defaultSuggestedFeeRecipient: { + description: + "Specify fee recipient default for collecting the EL block fees and rewards (a hex string representing 20 bytes address: ^0x[a-fA-F0-9]{40}$). It would be possible (WIP) to override this per validator key using config or keymanager API.", + default: defaultDefaultSuggestedFeeRecipient, + type: "string", + }, + importKeystoresPath: { description: "Path(s) to a directory or single filepath to validator keystores, i.e. Launchpad validators", defaultDescription: "./keystores/*.json", diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index 5c94c248f60b..23e7df575df6 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -1,10 +1,11 @@ import fs from "node:fs"; import {defaultOptions, IBeaconNodeOptions} from "@chainsafe/lodestar"; -import {ICliCommandOptions, extractJwtHexSecret} from "../../util"; +import {ICliCommandOptions, extractJwtHexSecret, parseFeeRecipientHex} from "../../util"; export type ExecutionEngineArgs = { "execution.urls": string[]; "execution.timeout": number; + defaultSuggestedFeeRecipient?: string; "jwt-secret"?: string; }; @@ -12,9 +13,21 @@ export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["execut return { urls: args["execution.urls"], timeout: args["execution.timeout"], + /** + * jwtSecret is parsed as hex instead of bytes because the merge with defaults + * in beaconOptions messes up the bytes array as as index => value object + */ jwtSecretHex: args["jwt-secret"] ? extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim()) : undefined, + /** + * defaultSuggestedFeeRecipient is parsed as hex instead of ExecutionAddress + * bytes because the merge with defaults in beaconOptions messes up the bytes + * array as index => value object + */ + defaultSuggestedFeeRecipientHex: args["defaultSuggestedFeeRecipient"] + ? parseFeeRecipientHex(args["defaultSuggestedFeeRecipient"]) + : undefined, }; } @@ -41,4 +54,15 @@ export const options: ICliCommandOptions = { type: "string", group: "execution", }, + + defaultSuggestedFeeRecipient: { + description: + "Specify fee recipient default for collecting the EL block fees and rewards (a hex string representing 20 bytes address: ^0x[a-fA-F0-9]{40}$) in case validator fails to update for a validator index before calling produceBlock.", + default: + defaultOptions.executionEngine.mode === "http" + ? String(defaultOptions.executionEngine.defaultSuggestedFeeRecipientHex) + : "", + type: "string", + group: "execution", + }, }; diff --git a/packages/cli/src/util/feeRecipient.ts b/packages/cli/src/util/feeRecipient.ts new file mode 100644 index 000000000000..4d6d1f3e5833 --- /dev/null +++ b/packages/cli/src/util/feeRecipient.ts @@ -0,0 +1,16 @@ +import {ExecutionAddress} from "@chainsafe/lodestar-types"; +import {fromHex} from "@chainsafe/lodestar-utils"; + +export function parseFeeRecipientHex(feeRecipientHexString: string): string { + const hexPattern = new RegExp(/^(0x|0X)(?[a-fA-F0-9]{40})$/, "g"); + const feeRecipientStringMatch = hexPattern.exec(feeRecipientHexString); + const feeRecipientString = feeRecipientStringMatch?.groups?.feeRecipientString; + if (feeRecipientString === undefined) + throw Error(`Invalid feeRecipient= ${feeRecipientHexString}, expected format: ^0x[a-fA-F0-9]{40}$`); + return feeRecipientString; +} + +export function parseFeeRecipient(feeRecipientHexString: string): ExecutionAddress { + const feeRecipientHex = parseFeeRecipientHex(feeRecipientHexString); + return fromHex(feeRecipientHex); +} diff --git a/packages/cli/src/util/index.ts b/packages/cli/src/util/index.ts index e7d6d32b62d8..8a118298fa9e 100644 --- a/packages/cli/src/util/index.ts +++ b/packages/cli/src/util/index.ts @@ -17,3 +17,4 @@ export * from "./stripOffNewlines"; export * from "./types"; export * from "./bls"; export * from "./jwt"; +export * from "./feeRecipient"; diff --git a/packages/cli/test/unit/validator/options.test.ts b/packages/cli/test/unit/validator/options.test.ts new file mode 100644 index 000000000000..7c2211c5dcd1 --- /dev/null +++ b/packages/cli/test/unit/validator/options.test.ts @@ -0,0 +1,28 @@ +import {expect} from "chai"; +import {parseFeeRecipient} from "../../../src/util"; + +const feeRecipient = Buffer.from(Array.from({length: 20}, () => Math.round(Math.random() * 255))); +const feeRecipientString = feeRecipient.toString("hex"); + +describe("parseFeeRecipient", () => { + const testCases: string[] = [`0x${feeRecipientString}`, `0X${feeRecipientString}`]; + for (const testCase of testCases) { + it(`parse ${testCase}`, () => { + expect(feeRecipient).to.be.deep.equal(parseFeeRecipient(testCase)); + }); + } +}); + +describe("invalid feeRecipient", () => { + const testCases: string[] = [ + feeRecipientString, + `X0${feeRecipientString}`, + `0x${feeRecipientString}13`, + `0x${feeRecipientString.substr(0, 38)}`, + ]; + for (const testCase of testCases) { + it(`should error on ${testCase}`, () => { + expect(() => parseFeeRecipient(testCase)).to.throw(); + }); + } +}); diff --git a/packages/lodestar/src/api/impl/validator/index.ts b/packages/lodestar/src/api/impl/validator/index.ts index b9c32476de80..7a3234d807a3 100644 --- a/packages/lodestar/src/api/impl/validator/index.ts +++ b/packages/lodestar/src/api/impl/validator/index.ts @@ -183,8 +183,6 @@ export function getValidatorApi({chain, config, logger, metrics, network, sync}: slot, randaoReveal, graffiti: toGraffitiBuffer(graffiti || ""), - // TODO - TEMP - feeRecipient: Buffer.alloc(20, 0), } ); metrics?.blockProductionSuccess.inc(); @@ -584,5 +582,9 @@ export function getValidatorApi({chain, config, logger, metrics, network, sync}: network.prepareSyncCommitteeSubnets(subs); }, + + async prepareBeaconProposer(proposers) { + await chain.executionEngine.updateProposerPreparation(chain.clock.currentEpoch, proposers); + }, }; } diff --git a/packages/lodestar/src/chain/factory/block/body.ts b/packages/lodestar/src/chain/factory/block/body.ts index bc4cb83d4ca0..ebcb5186612c 100644 --- a/packages/lodestar/src/chain/factory/block/body.ts +++ b/packages/lodestar/src/chain/factory/block/body.ts @@ -13,6 +13,7 @@ import { Slot, ssz, ExecutionAddress, + ValidatorIndex, } from "@chainsafe/lodestar-types"; import { CachedBeaconStateAllForks, @@ -37,14 +38,14 @@ export async function assembleBody( blockSlot, parentSlot, parentBlockRoot, - feeRecipient, + proposerIndex, }: { randaoReveal: Bytes96; graffiti: Bytes32; blockSlot: Slot; parentSlot: Slot; parentBlockRoot: Root; - feeRecipient: ExecutionAddress; + proposerIndex: ValidatorIndex; } ): Promise { // TODO: @@ -89,9 +90,21 @@ export async function assembleBody( // - Call prepareExecutionPayload again if parameters change const finalizedBlockHash = chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash; + const feeRecipient = chain.executionEngine.proposers.getOrDefault(proposerIndex).feeRecipient; // prepareExecutionPayload will throw error via notifyForkchoiceUpdate if // the EL returns Syncing on this request to prepare a payload + // + // TODO: + // The payloadId should be extracted from the ones cached in the execution engine + // by the advance firing of the fcU. If no entry in the cache is available then + // continue with the usual firing, but this will most likely not generate a full + // block. However some timing consideration can be done here to bundle some time + // for the same. + // + // For MeV boost integration as well, this is where the execution header will be + // fetched from the payload id and a blinded block will be produced instead of + // fullblock for the validator to sign const payloadId = await prepareExecutionPayload( chain, finalizedBlockHash ?? ZERO_HASH_HEX, diff --git a/packages/lodestar/src/chain/factory/block/index.ts b/packages/lodestar/src/chain/factory/block/index.ts index 210b81b894af..b2b6532c9f58 100644 --- a/packages/lodestar/src/chain/factory/block/index.ts +++ b/packages/lodestar/src/chain/factory/block/index.ts @@ -3,7 +3,7 @@ */ import {CachedBeaconStateAllForks, allForks} from "@chainsafe/lodestar-beacon-state-transition"; -import {Bytes32, Bytes96, ExecutionAddress, Root, Slot} from "@chainsafe/lodestar-types"; +import {Bytes32, Bytes96, Root, Slot} from "@chainsafe/lodestar-types"; import {fromHexString} from "@chainsafe/ssz"; import {ZERO_HASH} from "../../../constants"; @@ -23,21 +23,20 @@ export async function assembleBlock( randaoReveal, graffiti, slot, - feeRecipient, }: { randaoReveal: Bytes96; graffiti: Bytes32; slot: Slot; - feeRecipient: ExecutionAddress; } ): Promise { const head = chain.forkChoice.getHead(); const state = await chain.regen.getBlockSlotState(head.blockRoot, slot, RegenCaller.produceBlock); const parentBlockRoot = fromHexString(head.blockRoot); + const proposerIndex = state.epochCtx.getBeaconProposer(slot); const block: allForks.BeaconBlock = { slot, - proposerIndex: state.epochCtx.getBeaconProposer(slot), + proposerIndex, parentRoot: parentBlockRoot, stateRoot: ZERO_HASH, body: await assembleBody(chain, state, { @@ -46,7 +45,7 @@ export async function assembleBlock( blockSlot: slot, parentSlot: slot - 1, parentBlockRoot, - feeRecipient, + proposerIndex, }), }; diff --git a/packages/lodestar/src/executionEngine/disabled.ts b/packages/lodestar/src/executionEngine/disabled.ts index 4df867b29480..1b31c61b85c4 100644 --- a/packages/lodestar/src/executionEngine/disabled.ts +++ b/packages/lodestar/src/executionEngine/disabled.ts @@ -1,6 +1,13 @@ +import {ValidatorIndex, Epoch, ExecutionAddress} from "@chainsafe/lodestar-types"; +import {MapDef} from "../util/map"; import {IExecutionEngine} from "./interface"; export class ExecutionEngineDisabled implements IExecutionEngine { + readonly proposers = new MapDef(() => ({ + epoch: 0, + feeRecipient: Buffer.alloc(20, 0), + })); + async notifyNewPayload(): Promise { throw Error("Execution engine disabled"); } @@ -12,4 +19,8 @@ export class ExecutionEngineDisabled implements IExecutionEngine { async getPayload(): Promise { throw Error("Execution engine disabled"); } + + async updateProposerPreparation(): Promise { + throw Error("Execution engine disabled"); + } } diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index fa6ebd1ec973..031cdbe93e53 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -1,5 +1,5 @@ import {AbortSignal} from "@chainsafe/abort-controller"; -import {bellatrix, RootHex, Root} from "@chainsafe/lodestar-types"; +import {bellatrix, RootHex, Root, Epoch, ExecutionAddress, ValidatorIndex} from "@chainsafe/lodestar-types"; import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; import {fromHex} from "@chainsafe/lodestar-utils"; @@ -22,7 +22,9 @@ import { PayloadId, PayloadAttributes, ApiPayloadAttributes, + ProposerPreparationData, } from "./interface"; +import {MapDef} from "../util/map"; export type ExecutionEngineHttpOpts = { urls: string[]; @@ -34,8 +36,10 @@ export type ExecutionEngineHttpOpts = { * +-5 seconds interval. */ jwtSecretHex?: string; + defaultSuggestedFeeRecipientHex?: string; }; +export const defaultDefaultSuggestedFeeRecipient = "0x0000000000000000000000000000000000000000"; export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = { /** * By default ELs host engine api on an auth protected 8551 port, would need a jwt secret to be @@ -44,6 +48,7 @@ export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = { */ urls: ["http://localhost:8551"], timeout: 12000, + defaultSuggestedFeeRecipientHex: defaultDefaultSuggestedFeeRecipient, }; /** @@ -56,9 +61,15 @@ export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = { * https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.1/src/engine/interop/specification.md */ export class ExecutionEngineHttp implements IExecutionEngine { + readonly proposers: MapDef; private readonly rpc: IJsonRpcHttpClient; constructor(opts: ExecutionEngineHttpOpts, signal: AbortSignal, rpc?: IJsonRpcHttpClient) { + const feeRecipient = fromHex(opts.defaultSuggestedFeeRecipientHex ?? defaultDefaultSuggestedFeeRecipient); + this.proposers = new MapDef(() => ({ + epoch: 0, + feeRecipient, + })); this.rpc = rpc ?? new JsonRpcHttpClient(opts.urls, { @@ -271,6 +282,12 @@ export class ExecutionEngineHttp implements IExecutionEngine { return parseExecutionPayload(executionPayloadRpc); } + + async updateProposerPreparation(epoch: Epoch, proposers: ProposerPreparationData[]): Promise { + proposers.forEach(({validatorIndex, feeRecipient}) => { + this.proposers.set(validatorIndex, {epoch, feeRecipient}); + }); + } } /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/packages/lodestar/src/executionEngine/index.ts b/packages/lodestar/src/executionEngine/index.ts index a4dba9196e94..2f1e6fefe15b 100644 --- a/packages/lodestar/src/executionEngine/index.ts +++ b/packages/lodestar/src/executionEngine/index.ts @@ -1,10 +1,21 @@ import {AbortSignal} from "@chainsafe/abort-controller"; import {IExecutionEngine} from "./interface"; import {ExecutionEngineDisabled} from "./disabled"; -import {ExecutionEngineHttp, ExecutionEngineHttpOpts, defaultExecutionEngineHttpOpts} from "./http"; +import { + ExecutionEngineHttp, + ExecutionEngineHttpOpts, + defaultExecutionEngineHttpOpts, + defaultDefaultSuggestedFeeRecipient, +} from "./http"; import {ExecutionEngineMock, ExecutionEngineMockOpts} from "./mock"; -export {IExecutionEngine, ExecutionEngineHttp, ExecutionEngineDisabled, ExecutionEngineMock}; +export { + IExecutionEngine, + ExecutionEngineHttp, + ExecutionEngineDisabled, + ExecutionEngineMock, + defaultDefaultSuggestedFeeRecipient, +}; export type ExecutionEngineOpts = | ({mode?: "http"} & ExecutionEngineHttpOpts) diff --git a/packages/lodestar/src/executionEngine/interface.ts b/packages/lodestar/src/executionEngine/interface.ts index cf884988ab25..b54b99088415 100644 --- a/packages/lodestar/src/executionEngine/interface.ts +++ b/packages/lodestar/src/executionEngine/interface.ts @@ -1,4 +1,5 @@ -import {bellatrix, Root, RootHex} from "@chainsafe/lodestar-types"; +import {bellatrix, Root, RootHex, ValidatorIndex, Epoch, ExecutionAddress} from "@chainsafe/lodestar-types"; +import {MapDef} from "../util/map"; import {DATA, QUANTITY} from "../eth1/provider/utils"; // An execution engine can produce a payload id anywhere the the uint64 range @@ -63,6 +64,11 @@ export type ApiPayloadAttributes = { /** DATA, 20 Bytes - suggested value for the coinbase field of the new payload */ suggestedFeeRecipient: DATA; }; + +export type ProposerPreparationData = { + validatorIndex: ValidatorIndex; + feeRecipient: ExecutionAddress; +}; /** * Execution engine represents an abstract protocol to interact with execution clients. Potential transports include: * - JSON RPC over network @@ -70,6 +76,7 @@ export type ApiPayloadAttributes = { * - Integrated code into the same binary */ export interface IExecutionEngine { + proposers: MapDef; /** * A state transition function which applies changes to the self.execution_state. * Returns ``True`` iff ``execution_payload`` is valid with respect to ``self.execution_state``. @@ -107,4 +114,6 @@ export interface IExecutionEngine { * https://github.com/ethereum/consensus-specs/blob/dev/specs/merge/validator.md#get_payload */ getPayload(payloadId: PayloadId): Promise; + + updateProposerPreparation(currentEpoch: Epoch, proposers: ProposerPreparationData[]): Promise; } diff --git a/packages/lodestar/src/executionEngine/mock.ts b/packages/lodestar/src/executionEngine/mock.ts index f43aa48c5ce6..053e45edc455 100644 --- a/packages/lodestar/src/executionEngine/mock.ts +++ b/packages/lodestar/src/executionEngine/mock.ts @@ -1,6 +1,8 @@ import crypto from "node:crypto"; -import {bellatrix, RootHex, Root} from "@chainsafe/lodestar-types"; +import {bellatrix, RootHex, Root, Epoch, ValidatorIndex, ExecutionAddress} from "@chainsafe/lodestar-types"; import {toHexString} from "@chainsafe/ssz"; +import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; + import {ZERO_HASH, ZERO_HASH_HEX} from "../constants"; import { ExecutePayloadStatus, @@ -8,8 +10,10 @@ import { IExecutionEngine, PayloadId, PayloadAttributes, + ProposerPreparationData, } from "./interface"; -import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; +import {MapDef} from "../util/map"; + const INTEROP_GAS_LIMIT = 30e6; export type ExecutionEngineMockOpts = { @@ -23,12 +27,17 @@ export class ExecutionEngineMock implements IExecutionEngine { // Public state to check if notifyForkchoiceUpdate() is called properly headBlockRoot = ZERO_HASH_HEX; finalizedBlockRoot = ZERO_HASH_HEX; + readonly proposers: MapDef; private knownBlocks = new Map(); private preparingPayloads = new Map(); private payloadId = 0; constructor(opts: ExecutionEngineMockOpts) { + this.proposers = new MapDef(() => ({ + epoch: 0, + feeRecipient: Buffer.alloc(20, 0), + })); this.knownBlocks.set(opts.genesisBlockHash, { parentHash: ZERO_HASH, feeRecipient: Buffer.alloc(20, 0), @@ -140,4 +149,8 @@ export class ExecutionEngineMock implements IExecutionEngine { this.preparingPayloads.delete(payloadIdNbr); return payload; } + + async updateProposerPreparation(_epoch: Epoch, _proposers: ProposerPreparationData[]): Promise { + return; + } } diff --git a/packages/lodestar/src/node/options.ts b/packages/lodestar/src/node/options.ts index 56a436309ea7..70c4b84d326e 100644 --- a/packages/lodestar/src/node/options.ts +++ b/packages/lodestar/src/node/options.ts @@ -10,9 +10,10 @@ import {defaultLoggerOptions, IBeaconLoggerOptions} from "./loggerOptions"; import {defaultMetricsOptions, MetricsOptions} from "../metrics/options"; import {defaultNetworkOptions, INetworkOptions} from "../network/options"; import {defaultSyncOptions, SyncOptions} from "../sync/options"; -import {defaultExecutionEngineOpts, ExecutionEngineOpts} from "../executionEngine"; +import {defaultExecutionEngineOpts, ExecutionEngineOpts, defaultDefaultSuggestedFeeRecipient} from "../executionEngine"; // Re-export so the CLI doesn't need to depend on lodestar-api export {allNamespaces} from "../api/rest/index"; +export {defaultDefaultSuggestedFeeRecipient}; export interface IBeaconNodeOptions { api: IApiOptions; diff --git a/packages/lodestar/test/unit/chain/factory/block/blockAssembly.test.ts b/packages/lodestar/test/unit/chain/factory/block/blockAssembly.test.ts index e99058d8ed11..a9b287b3544f 100644 --- a/packages/lodestar/test/unit/chain/factory/block/blockAssembly.test.ts +++ b/packages/lodestar/test/unit/chain/factory/block/blockAssembly.test.ts @@ -64,7 +64,6 @@ describe("block assembly", function () { randaoReveal: Buffer.alloc(96, 0), graffiti: Buffer.alloc(32, 0), slot: 1, - feeRecipient: Buffer.alloc(20, 0), } ); expect(result).to.not.be.null; diff --git a/packages/lodestar/test/unit/chain/factory/block/body.test.ts b/packages/lodestar/test/unit/chain/factory/block/body.test.ts index 95abd865f3c6..7726b8e684ef 100644 --- a/packages/lodestar/test/unit/chain/factory/block/body.test.ts +++ b/packages/lodestar/test/unit/chain/factory/block/body.test.ts @@ -59,7 +59,7 @@ describe("blockAssembly - body", function () { blockSlot: 1, parentSlot: 0, parentBlockRoot: Buffer.alloc(32, 0), - feeRecipient: Buffer.alloc(20, 0), + proposerIndex: 1000, }); expect(result).to.not.be.null; expect(result.randaoReveal.length).to.be.equal(96); diff --git a/packages/lodestar/test/utils/node/validator.ts b/packages/lodestar/test/utils/node/validator.ts index e90752334a8b..3d9645b46474 100644 --- a/packages/lodestar/test/utils/node/validator.ts +++ b/packages/lodestar/test/utils/node/validator.ts @@ -11,14 +11,16 @@ import { ISlashingProtection, SignerLocal, } from "@chainsafe/lodestar-validator"; -import {BeaconNode} from "../../../src/node"; -import {testLogger, TestLoggerOpts} from "../logger"; + import {SecretKey} from "@chainsafe/bls"; import {getLocalSecretKeys} from "@chainsafe/lodestar-cli/src/cmds/validator/keys"; import {IValidatorCliArgs} from "@chainsafe/lodestar-cli/src/cmds/validator/options"; import {IGlobalArgs} from "@chainsafe/lodestar-cli/src/options"; import {KEY_IMPORTED_PREFIX} from "@chainsafe/lodestar-keymanager-server"; +import {BeaconNode} from "../../../src/node"; +import {testLogger, TestLoggerOpts} from "../logger"; + export async function getAndInitValidatorsWithKeystore({ node, keystoreContent, diff --git a/packages/validator/src/services/prepareBeaconProposer.ts b/packages/validator/src/services/prepareBeaconProposer.ts new file mode 100644 index 000000000000..c85c628bc6a6 --- /dev/null +++ b/packages/validator/src/services/prepareBeaconProposer.ts @@ -0,0 +1,54 @@ +import {ValidatorIndex, ExecutionAddress, Epoch} from "@chainsafe/lodestar-types"; +import {Api} from "@chainsafe/lodestar-api"; + +import {ValidatorStore} from "./validatorStore"; +import {IndicesService} from "./indices"; +import {IClock, ILoggerVc} from "../util"; +import {Metrics} from "../metrics"; + +type ProposerPreparationData = { + validatorIndex: ValidatorIndex; + feeRecipient: ExecutionAddress; +}; + +/** + * This service is responsible for updating the BNs and/or Mev relays with + * the corresponding feeRecipient suggestion. This should ideally run per epoch + * but can be run per slot. Lighthouse also uses this to trigger any block + */ +export class PrepareBeaconProposerService { + constructor( + private readonly logger: ILoggerVc, + private readonly api: Api, + private clock: IClock, + private readonly validatorStore: ValidatorStore, + private readonly defaultSuggestedFeeRecipient: ExecutionAddress, + private readonly indicesService: IndicesService, + private readonly metrics: Metrics | null + ) { + clock.runEverySlot(this.prepareBeaconProposer); + } + + private prepareBeaconProposer = async (epoch: Epoch): Promise => { + await Promise.all([ + // Run prepareBeaconProposer immediately for all known local indices + this.api.validator + .prepareBeaconProposer(this.getProposerData(this.indicesService.getAllLocalIndices())) + .catch((e: Error) => { + this.logger.error("Error on prepareBeaconProposer", {epoch}, e); + }), + + // At the same time fetch any remaining unknown validator indices, then poll duties for those newIndices only + this.indicesService + .pollValidatorIndices() + .then((newIndices) => this.api.validator.prepareBeaconProposer(this.getProposerData(newIndices))) + .catch((e: Error) => { + this.logger.error("Error on poll indices and prepareBeaconProposer", {epoch}, e); + }), + ]); + }; + + private getProposerData(indices: number[]): ProposerPreparationData[] { + return indices.map((validatorIndex) => ({validatorIndex, feeRecipient: this.defaultSuggestedFeeRecipient})); + } +} diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index 7941b1b0f283..fb1c8cdcb0dd 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -1,6 +1,6 @@ import {AbortController, AbortSignal} from "@chainsafe/abort-controller"; import {IDatabaseApiOptions} from "@chainsafe/lodestar-db"; -import {ssz} from "@chainsafe/lodestar-types"; +import {ssz, ExecutionAddress} from "@chainsafe/lodestar-types"; import {createIBeaconConfig, IBeaconConfig} from "@chainsafe/lodestar-config"; import {Genesis} from "@chainsafe/lodestar-types/phase0"; import {ILogger} from "@chainsafe/lodestar-utils"; @@ -11,6 +11,7 @@ import {BlockProposingService} from "./services/block"; import {AttestationService} from "./services/attestation"; import {IndicesService} from "./services/indices"; import {SyncCommitteeService} from "./services/syncCommittee"; +import {PrepareBeaconProposerService} from "./services/prepareBeaconProposer"; import {ISlashingProtection} from "./slashingProtection"; import {assertEqualParams, getLoggerVc, NotEqualParamsError} from "./util"; import {ChainHeaderTracker} from "./services/chainHeaderTracker"; @@ -30,6 +31,7 @@ export type ValidatorOptions = { logger: ILogger; afterBlockDelaySlotFraction?: number; graffiti?: string; + defaultSuggestedFeeRecipient?: ExecutionAddress; }; // TODO: Extend the timeout, and let it be customizable @@ -52,6 +54,7 @@ export class Validator { private readonly attestationService: AttestationService; private readonly syncCommitteeService: SyncCommitteeService; private readonly indicesService: IndicesService; + private readonly prepareBeaconProposerService: PrepareBeaconProposerService | null; private readonly config: IBeaconConfig; private readonly api: Api; private readonly clock: IClock; @@ -61,7 +64,7 @@ export class Validator { private state: State = {status: Status.stopped}; constructor(opts: ValidatorOptions, readonly genesis: Genesis, metrics: Metrics | null = null) { - const {dbOps, logger, slashingProtection, signers, graffiti} = opts; + const {dbOps, logger, slashingProtection, signers, graffiti, defaultSuggestedFeeRecipient} = opts; const config = createIBeaconConfig(dbOps.config, genesis.genesisValidatorsRoot); const api = @@ -111,6 +114,18 @@ export class Validator { metrics ); + this.prepareBeaconProposerService = defaultSuggestedFeeRecipient + ? new PrepareBeaconProposerService( + loggerVc, + api, + clock, + validatorStore, + defaultSuggestedFeeRecipient, + indicesService, + metrics + ) + : null; + this.config = config; this.logger = logger; this.api = api;