diff --git a/packages/api/src/routes/validator.ts b/packages/api/src/routes/validator.ts index aaf97c8867ff..154fa6bf1810 100644 --- a/packages/api/src/routes/validator.ts +++ b/packages/api/src/routes/validator.ts @@ -23,6 +23,7 @@ import { WithVersion, reqOnlyBody, ReqSerializers, + jsonType, } from "../utils"; // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes @@ -44,6 +45,15 @@ export type SyncCommitteeSubscription = { untilEpoch: Epoch; }; +/** + * The types used here are string instead of ssz based because the use of proposer data + * is just validator --> beacon json api call for `beaconProposerCache` cache update. + */ +export type ProposerPreparationData = { + validatorIndex: string; + feeRecipient: string; +}; + export type ProposerDuty = { slot: Slot; validatorIndex: ValidatorIndex; @@ -197,6 +207,8 @@ export type Api = { prepareBeaconCommitteeSubnet(subscriptions: BeaconCommitteeSubscription[]): Promise; prepareSyncCommitteeSubnets(subscriptions: SyncCommitteeSubscription[]): Promise; + + prepareBeaconProposer(proposers: ProposerPreparationData[]): Promise; }; /** @@ -215,6 +227,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 +244,7 @@ export type ReqTypes = { publishContributionAndProofs: {body: unknown}; prepareBeaconCommitteeSubnet: {body: unknown}; prepareSyncCommitteeSubnets: {body: unknown}; + prepareBeaconProposer: {body: unknown}; }; export function getReqSerializers(): ReqSerializers { @@ -330,6 +344,13 @@ 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: { + writeReq: (items: ProposerPreparationData[]) => ({body: items.map((item) => jsonType("snake").toJson(item))}), + parseReq: ({body}) => [ + (body as Record[]).map((item) => jsonType("snake").fromJson(item) as ProposerPreparationData), + ], + schema: {body: Schema.ObjectArray}, + }, }; } diff --git a/packages/api/test/unit/validator.test.ts b/packages/api/test/unit/validator.test.ts index 532a90c19550..97058514fa49 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: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"}]], + 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..b02e909ad2f5 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, defaultDefaultFeeRecipient} from "./options"; import {getLocalSecretKeys, getExternalSigners, groupExternalSignersByUrl} from "./keys"; /** @@ -21,6 +21,7 @@ export async function validatorHandler(args: IValidatorCliArgs & IGlobalArgs): P await initBLS(); const graffiti = args.graffiti || getDefaultGraffiti(); + const defaultFeeRecipient = parseFeeRecipient(args.defaultFeeRecipient ?? defaultDefaultFeeRecipient); const validatorPaths = getValidatorPaths(args); const beaconPaths = getBeaconPaths(args); @@ -129,6 +130,7 @@ export async function validatorHandler(args: IValidatorCliArgs & IGlobalArgs): P signers, graffiti, afterBlockDelaySlotFraction: args.afterBlockDelaySlotFraction, + defaultFeeRecipient, }, controller.signal, metrics diff --git a/packages/cli/src/cmds/validator/options.ts b/packages/cli/src/cmds/validator/options.ts index 9eec393c894e..0ad29149459c 100644 --- a/packages/cli/src/cmds/validator/options.ts +++ b/packages/cli/src/cmds/validator/options.ts @@ -1,3 +1,4 @@ +import {defaultOptions} from "@chainsafe/lodestar"; import {ICliCommandOptions, ILogArgs} from "../../util"; import {defaultValidatorPaths} from "./paths"; import {accountValidatorOptions, IAccountValidatorArgs} from "../account/cmds/validator/options"; @@ -11,6 +12,8 @@ export const validatorMetricsDefaultOptions = { address: "127.0.0.1", }; +export const defaultDefaultFeeRecipient = defaultOptions.chain.defaultFeeRecipient; + export type IValidatorCliArgs = IAccountValidatorArgs & ILogArgs & { logFile: IBeaconPaths["logFile"]; @@ -19,6 +22,9 @@ export type IValidatorCliArgs = IAccountValidatorArgs & force: boolean; graffiti: string; afterBlockDelaySlotFraction?: number; + defaultFeeRecipient?: string; + strictFeeRecipientCheck?: boolean; + importKeystoresPath?: string[]; importKeystoresPassword?: string; externalSignerUrl?: string; @@ -68,6 +74,18 @@ export const validatorOptions: ICliCommandOptions = { type: "number", }, + defaultFeeRecipient: { + 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.", + defaultDescription: defaultDefaultFeeRecipient, + type: "string", + }, + + strictFeeRecipientCheck: { + description: "Enable strict checking of the validator's feeRecipient with the one returned by engine", + type: "boolean", + }, + 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/chain.ts b/packages/cli/src/options/beaconNodeOptions/chain.ts index f3f4e919aa35..b9205ce085e7 100644 --- a/packages/cli/src/options/beaconNodeOptions/chain.ts +++ b/packages/cli/src/options/beaconNodeOptions/chain.ts @@ -7,6 +7,7 @@ export interface IChainArgs { "chain.disableBlsBatchVerify": boolean; "chain.persistInvalidSszObjects": boolean; "chain.proposerBoostEnabled": boolean; + "chain.defaultFeeRecipient": string; "safe-slots-to-import-optimistically": number; // this is defined as part of IBeaconPaths // "chain.persistInvalidSszObjectsDir": string; @@ -21,6 +22,7 @@ export function parseArgs(args: IChainArgs): IBeaconNodeOptions["chain"] { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any persistInvalidSszObjectsDir: undefined as any, proposerBoostEnabled: args["chain.proposerBoostEnabled"], + defaultFeeRecipient: args["chain.defaultFeeRecipient"], safeSlotsToImportOptimistically: args["safe-slots-to-import-optimistically"], }; } @@ -66,6 +68,14 @@ Will double processing times. Use only for debugging purposes.", group: "chain", }, + "chain.defaultFeeRecipient": { + 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.", + defaultDescription: defaultOptions.chain.defaultFeeRecipient, + type: "string", + group: "chain", + }, + "safe-slots-to-import-optimistically": { hidden: true, type: "number", diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index 5c94c248f60b..ab8a00939685 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -12,6 +12,10 @@ 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, diff --git a/packages/cli/src/util/feeRecipient.ts b/packages/cli/src/util/feeRecipient.ts new file mode 100644 index 000000000000..2706bfafdfc5 --- /dev/null +++ b/packages/cli/src/util/feeRecipient.ts @@ -0,0 +1,6 @@ +export function parseFeeRecipient(feeRecipientHex: string): string { + if (!/^0x[a-fA-F0-9]{40}$/i.test(feeRecipientHex)) { + throw Error(`Invalid feeRecipient= ${feeRecipientHex}, expected format: ^0x[a-fA-F0-9]{40}$`); + } + return feeRecipientHex.toLowerCase(); +} 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/options/beaconNodeOptions.test.ts b/packages/cli/test/unit/options/beaconNodeOptions.test.ts index 9ba26b98f431..2fdba3b22554 100644 --- a/packages/cli/test/unit/options/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/options/beaconNodeOptions.test.ts @@ -21,6 +21,7 @@ describe("options / beaconNodeOptions", () => { "chain.disableBlsBatchVerify": true, "chain.persistInvalidSszObjects": true, "chain.proposerBoostEnabled": false, + "chain.defaultFeeRecipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "safe-slots-to-import-optimistically": 256, "eth1.enabled": true, @@ -79,6 +80,7 @@ describe("options / beaconNodeOptions", () => { persistInvalidSszObjects: true, proposerBoostEnabled: false, safeSlotsToImportOptimistically: 256, + defaultFeeRecipient: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, eth1: { enabled: true, 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..f59f2f78bf59 --- /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("validator / parseFeeRecipient", () => { + const testCases: string[] = [`0x${feeRecipientString}`, `0X${feeRecipientString}`]; + for (const testCase of testCases) { + it(`parse ${testCase}`, () => { + expect(`0x${feeRecipientString}`).to.be.deep.equal(parseFeeRecipient(testCase)); + }); + } +}); + +describe("validator / 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..603c004d3c1f 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.updateBeaconProposerData(chain.clock.currentEpoch, proposers); + }, }; } diff --git a/packages/lodestar/src/chain/beaconProposerCache.ts b/packages/lodestar/src/chain/beaconProposerCache.ts new file mode 100644 index 000000000000..6722e7bca410 --- /dev/null +++ b/packages/lodestar/src/chain/beaconProposerCache.ts @@ -0,0 +1,36 @@ +import {MapDef} from "../util/map"; +import {Epoch} from "@chainsafe/lodestar-types"; +import {IMetrics} from "../metrics"; +import {routes} from "@chainsafe/lodestar-api"; + +const PROPOSER_PRESERVE_EPOCHS = 2; + +export type ProposerPreparationData = routes.validator.ProposerPreparationData; + +export class BeaconProposerCache { + private readonly feeRecipientByValidatorIndex: MapDef; + constructor(opts: {defaultFeeRecipient: string}, private readonly metrics?: IMetrics | null) { + this.feeRecipientByValidatorIndex = new MapDef(() => ({ + epoch: 0, + feeRecipient: opts.defaultFeeRecipient, + })); + } + + add(epoch: Epoch, {validatorIndex, feeRecipient}: ProposerPreparationData): void { + this.feeRecipientByValidatorIndex.set(validatorIndex, {epoch, feeRecipient}); + } + + prune(epoch: Epoch): void { + // This is not so optimized function, but could maintain a 2d array may be? + for (const [validatorIndex, feeRecipientEntry] of this.feeRecipientByValidatorIndex.entries()) { + // We only retain an entry for PROPOSER_PRESERVE_EPOCHS epochs + if (feeRecipientEntry.epoch + PROPOSER_PRESERVE_EPOCHS < epoch) { + this.feeRecipientByValidatorIndex.delete(validatorIndex); + } + } + } + + get(proposerIndex: number | string): string { + return this.feeRecipientByValidatorIndex.getOrDefault(`${proposerIndex}`).feeRecipient; + } +} diff --git a/packages/lodestar/src/chain/chain.ts b/packages/lodestar/src/chain/chain.ts index b89b81c202cf..6805d957c988 100644 --- a/packages/lodestar/src/chain/chain.ts +++ b/packages/lodestar/src/chain/chain.ts @@ -13,7 +13,7 @@ import { } from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; -import {allForks, UintNum64, Root, phase0, Slot, RootHex} from "@chainsafe/lodestar-types"; +import {allForks, UintNum64, Root, phase0, Slot, RootHex, Epoch} from "@chainsafe/lodestar-types"; import {ILogger} from "@chainsafe/lodestar-utils"; import {fromHexString} from "@chainsafe/ssz"; import {AbortController} from "@chainsafe/abort-controller"; @@ -25,7 +25,7 @@ import {BlockProcessor, PartiallyVerifiedBlockFlags} from "./blocks"; import {IBeaconClock, LocalClock} from "./clock"; import {ChainEventEmitter} from "./emitter"; import {handleChainEvents} from "./eventHandlers"; -import {IBeaconChain, SSZObjectType} from "./interface"; +import {IBeaconChain, SSZObjectType, ProposerPreparationData} from "./interface"; import {IChainOptions} from "./options"; import {IStateRegenerator, QueuedStateRegenerator, RegenCaller} from "./regen"; import {initializeForkChoice} from "./forkChoice"; @@ -52,6 +52,7 @@ import {IExecutionEngine} from "../executionEngine"; import {PrecomputeNextEpochTransitionScheduler} from "./precomputeNextEpochTransition"; import {ReprocessController} from "./reprocess"; import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof"; +import {BeaconProposerCache} from "./beaconProposerCache"; export class BeaconChain implements IBeaconChain { readonly genesisTime: UintNum64; @@ -91,6 +92,8 @@ export class BeaconChain implements IBeaconChain { readonly pubkey2index: PubkeyIndexMap; readonly index2pubkey: Index2PubkeyCache; + readonly beaconProposerCache: BeaconProposerCache; + protected readonly blockProcessor: BlockProcessor; protected readonly db: IBeaconDb; protected readonly logger: ILogger; @@ -148,6 +151,8 @@ export class BeaconChain implements IBeaconChain { this.pubkey2index = new PubkeyIndexMap(); this.index2pubkey = []; + this.beaconProposerCache = new BeaconProposerCache(opts); + // Restore state caches const cachedState = createCachedBeaconState(anchorState, { config, @@ -316,4 +321,10 @@ export class BeaconChain implements IBeaconChain { } return fileName; } + + async updateBeaconProposerData(epoch: Epoch, proposers: ProposerPreparationData[]): Promise { + proposers.forEach((proposer) => { + this.beaconProposerCache.add(epoch, proposer); + }); + } } diff --git a/packages/lodestar/src/chain/eventHandlers.ts b/packages/lodestar/src/chain/eventHandlers.ts index a16c32e66cf9..b2b90fbd724a 100644 --- a/packages/lodestar/src/chain/eventHandlers.ts +++ b/packages/lodestar/src/chain/eventHandlers.ts @@ -97,6 +97,7 @@ export function onClockEpoch(this: BeaconChain, currentEpoch: Epoch): void { this.seenAttesters.prune(currentEpoch); this.seenAggregators.prune(currentEpoch); this.seenAggregatedAttestations.prune(currentEpoch); + this.beaconProposerCache.prune(currentEpoch); } export function onForkVersion(this: BeaconChain, version: Version): void { diff --git a/packages/lodestar/src/chain/factory/block/body.ts b/packages/lodestar/src/chain/factory/block/body.ts index bc4cb83d4ca0..e22db7c48b15 100644 --- a/packages/lodestar/src/chain/factory/block/body.ts +++ b/packages/lodestar/src/chain/factory/block/body.ts @@ -12,7 +12,7 @@ import { RootHex, Slot, ssz, - ExecutionAddress, + ValidatorIndex, } from "@chainsafe/lodestar-types"; import { CachedBeaconStateAllForks, @@ -37,14 +37,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 +89,21 @@ export async function assembleBody( // - Call prepareExecutionPayload again if parameters change const finalizedBlockHash = chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash; + const feeRecipient = chain.beaconProposerCache.get(proposerIndex); // 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, @@ -121,7 +133,7 @@ async function prepareExecutionPayload( chain: IBeaconChain, finalizedBlockHash: RootHex, state: CachedBeaconStateBellatrix, - suggestedFeeRecipient: ExecutionAddress + suggestedFeeRecipient: string ): Promise { // Use different POW block hash parent for block production based on merge status. // Returned value of null == using an empty ExecutionPayload value 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/chain/interface.ts b/packages/lodestar/src/chain/interface.ts index 4ff772d2fdee..6c29f52219df 100644 --- a/packages/lodestar/src/chain/interface.ts +++ b/packages/lodestar/src/chain/interface.ts @@ -1,4 +1,4 @@ -import {allForks, UintNum64, Root, phase0, Slot, RootHex} from "@chainsafe/lodestar-types"; +import {allForks, UintNum64, Root, phase0, Slot, RootHex, Epoch} from "@chainsafe/lodestar-types"; import {CachedBeaconStateAllForks} from "@chainsafe/lodestar-beacon-state-transition"; import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; @@ -23,6 +23,7 @@ import {AggregatedAttestationPool} from "./opPools/aggregatedAttestationPool"; import {PartiallyVerifiedBlockFlags} from "./blocks/types"; import {ReprocessController} from "./reprocess"; import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof"; +import {BeaconProposerCache, ProposerPreparationData} from "./beaconProposerCache"; export type Eth2Context = { activeValidatorCount: number; @@ -30,6 +31,8 @@ export type Eth2Context = { currentEpoch: number; }; +export {ProposerPreparationData}; + /** * The IBeaconChain service deals with processing incoming blocks, advancing a state transition * and applying the fork choice rule to update the chain head @@ -70,6 +73,8 @@ export interface IBeaconChain { readonly seenSyncCommitteeMessages: SeenSyncCommitteeMessages; readonly seenContributionAndProof: SeenContributionAndProof; + readonly beaconProposerCache: BeaconProposerCache; + /** Stop beacon chain processing */ close(): void; /** Populate in-memory caches with persisted data. Call at least once on startup */ @@ -99,6 +104,8 @@ export interface IBeaconChain { /** Persist bad items to persistInvalidSszObjectsDir dir, for example invalid state, attestations etc. */ persistInvalidSszObject(type: SSZObjectType, bytes: Uint8Array, suffix: string): string | null; + + updateBeaconProposerData(epoch: Epoch, proposers: ProposerPreparationData[]): Promise; } export type SSZObjectType = diff --git a/packages/lodestar/src/chain/options.ts b/packages/lodestar/src/chain/options.ts index 4c928c06ef5b..d65164646384 100644 --- a/packages/lodestar/src/chain/options.ts +++ b/packages/lodestar/src/chain/options.ts @@ -1,4 +1,6 @@ import {SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY} from "@chainsafe/lodestar-params"; +import {defaultDefaultFeeRecipient} from "@chainsafe/lodestar-validator"; + import {ArchiverOpts} from "./archiver"; import {ForkChoiceOpts} from "./forkChoice"; @@ -10,6 +12,7 @@ export type IChainOptions = BlockProcessOpts & blsVerifyAllMultiThread?: boolean; persistInvalidSszObjects?: boolean; persistInvalidSszObjectsDir?: string; + defaultFeeRecipient: string; }; export type BlockProcessOpts = { @@ -32,4 +35,5 @@ export const defaultChainOptions: IChainOptions = { persistInvalidSszObjectsDir: "", proposerBoostEnabled: true, safeSlotsToImportOptimistically: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY, + defaultFeeRecipient: defaultDefaultFeeRecipient, }; diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index fa6ebd1ec973..e24bd18d05b2 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -200,7 +200,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { ? { timestamp: numToQuantity(payloadAttributes.timestamp), prevRandao: bytesToData(payloadAttributes.prevRandao), - suggestedFeeRecipient: bytesToData(payloadAttributes.suggestedFeeRecipient), + suggestedFeeRecipient: payloadAttributes.suggestedFeeRecipient, } : undefined; diff --git a/packages/lodestar/src/executionEngine/interface.ts b/packages/lodestar/src/executionEngine/interface.ts index cf884988ab25..88a9b677e4ed 100644 --- a/packages/lodestar/src/executionEngine/interface.ts +++ b/packages/lodestar/src/executionEngine/interface.ts @@ -52,7 +52,9 @@ export type ForkChoiceUpdateStatus = export type PayloadAttributes = { timestamp: number; prevRandao: Uint8Array; - suggestedFeeRecipient: Uint8Array; + // DATA is anyway a hex string, so we can just track it as a hex string to + // avoid any conversions + suggestedFeeRecipient: string; }; export type ApiPayloadAttributes = { @@ -63,6 +65,7 @@ export type ApiPayloadAttributes = { /** DATA, 20 Bytes - suggested value for the coinbase field of the new payload */ suggestedFeeRecipient: DATA; }; + /** * Execution engine represents an abstract protocol to interact with execution clients. Potential transports include: * - JSON RPC over network diff --git a/packages/lodestar/src/executionEngine/mock.ts b/packages/lodestar/src/executionEngine/mock.ts index f43aa48c5ce6..72c3eba40ace 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 {toHexString} from "@chainsafe/ssz"; +import {toHexString, fromHexString} from "@chainsafe/ssz"; +import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; + import {ZERO_HASH, ZERO_HASH_HEX} from "../constants"; import { ExecutePayloadStatus, @@ -9,7 +11,7 @@ import { PayloadId, PayloadAttributes, } from "./interface"; -import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; + const INTEROP_GAS_LIMIT = 30e6; export type ExecutionEngineMockOpts = { @@ -105,7 +107,7 @@ export class ExecutionEngineMock implements IExecutionEngine { const payloadId = this.payloadId++; const payload: bellatrix.ExecutionPayload = { parentHash: headBlockHash, - feeRecipient: payloadAttributes.suggestedFeeRecipient, + feeRecipient: fromHexString(payloadAttributes.suggestedFeeRecipient), stateRoot: crypto.randomBytes(32), receiptsRoot: crypto.randomBytes(32), logsBloom: crypto.randomBytes(BYTES_PER_LOGS_BLOOM), diff --git a/packages/lodestar/test/perf/chain/verifyImportBlocks.test.ts b/packages/lodestar/test/perf/chain/verifyImportBlocks.test.ts index b9b16ea7d8f1..899717d33d4c 100644 --- a/packages/lodestar/test/perf/chain/verifyImportBlocks.test.ts +++ b/packages/lodestar/test/perf/chain/verifyImportBlocks.test.ts @@ -7,13 +7,15 @@ import { import {rangeSyncTest} from "@chainsafe/lodestar-beacon-state-transition/test/perf/params"; import {config} from "@chainsafe/lodestar-config/default"; import {SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY, SLOTS_PER_EPOCH} from "@chainsafe/lodestar-params"; +import {LevelDbController} from "@chainsafe/lodestar-db"; +import {defaultDefaultFeeRecipient} from "@chainsafe/lodestar-validator"; + import {BeaconChain} from "../../../src/chain"; import {ExecutionEngineDisabled} from "../../../src/executionEngine"; import {Eth1ForBlockProductionDisabled} from "../../../src/eth1"; import {testLogger} from "../../utils/logger"; import {linspace} from "../../../src/util/numpy"; import {BeaconDb} from "../../../src"; -import {LevelDbController} from "@chainsafe/lodestar-db"; // Define this params in `packages/beacon-state-transition/test/perf/params.ts` // to trigger Github actions CI cache @@ -80,6 +82,7 @@ describe("verify+import blocks - range sync perf test", () => { proposerBoostEnabled: true, safeSlotsToImportOptimistically: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY, disableArchiveOnCheckpoint: true, + defaultFeeRecipient: defaultDefaultFeeRecipient, }, { config: state.config, diff --git a/packages/lodestar/test/sim/merge-interop.test.ts b/packages/lodestar/test/sim/merge-interop.test.ts index cab4decc7ea2..85380687cfba 100644 --- a/packages/lodestar/test/sim/merge-interop.test.ts +++ b/packages/lodestar/test/sim/merge-interop.test.ts @@ -147,13 +147,13 @@ describe("executionEngine / ExecutionEngineHttp", function () { if (TX_SCENARIOS.includes("simple")) { await sendTransaction(jsonRpcUrl, { from: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - to: "0xafa3f8684e54059998bc3a7b0d2b0da075154d66", + to: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", gas: "0x76c0", gasPrice: "0x9184e72a000", value: "0x9184e72a", }); - const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + const balance = await getBalance(jsonRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); if (balance != "0x0") throw new Error("Invalid Balance: " + balance); } @@ -170,7 +170,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { // Note: this is created with a pre-defined genesis.json timestamp: quantityToNum("0x5"), prevRandao: dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), - suggestedFeeRecipient: dataToBytes("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"), + suggestedFeeRecipient: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", }; const finalizedBlockHash = "0x0000000000000000000000000000000000000000000000000000000000000000"; @@ -192,7 +192,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { if (TX_SCENARIOS.includes("simple")) { if (payload.transactions.length !== 1) throw new Error("Expected a simple transaction to be in the fetched payload"); - const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + const balance = await getBalance(jsonRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); if (balance != "0x0") throw new Error("Invalid Balance: " + balance); } @@ -215,7 +215,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { await executionEngine.notifyForkchoiceUpdate(bytesToData(payload.blockHash), genesisBlockHash); if (TX_SCENARIOS.includes("simple")) { - const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + const balance = await getBalance(jsonRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); if (balance !== "0x9184e72a") throw new Error("Invalid Balance"); } @@ -324,6 +324,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { // Now eth deposit/merge tracker methods directly available on engine endpoints eth1: {enabled: true, providerUrls: [engineApiUrl], jwtSecretHex}, executionEngine: {urls: [engineApiUrl], jwtSecretHex}, + chain: {defaultFeeRecipient: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, }, validatorCount: validatorClientCount * validatorsPerClient, logger: loggerNodeA, @@ -346,6 +347,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { // At least one sim test must use the REST API for beacon <-> validator comms useRestApi: true, testLoggerOpts, + defaultFeeRecipient: "0xcccccccccccccccccccccccccccccccccccccccc", // TODO test merge-interop with remote; }); @@ -359,7 +361,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { // If bellatrixEpoch > 0, this is the case of pre-merge transaction submission on EL pow await sendTransaction(jsonRpcUrl, { from: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - to: "0xafa3f8684e54059998bc3a7b0d2b0da075154d66", + to: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", gas: "0x76c0", gasPrice: "0x9184e72a000", value: "0x9184e72a", @@ -374,7 +376,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { // If bellatrixEpoch > 0, this is the case of pre-merge transaction confirmation on EL pow case 2: if (TX_SCENARIOS.includes("simple")) { - const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + const balance = await getBalance(jsonRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); if (balance !== "0x9184e72a") reject("Invalid Balance"); } break; @@ -390,7 +392,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { if (TX_SCENARIOS.includes("simple")) { await sendTransaction(jsonRpcUrl, { from: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - to: "0xafa3f8684e54059998bc3a7b0d2b0da075154d66", + to: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", gas: "0x76c0", gasPrice: "0x9184e72a000", value: "0x9184e72a", @@ -419,6 +421,10 @@ describe("executionEngine / ExecutionEngineHttp", function () { await bn.close(); await sleep(500); + if (bn.chain.beaconProposerCache.get(1) !== "0xcccccccccccccccccccccccccccccccccccccccc") { + throw Error("Invalid feeRecipient set at BN"); + } + // Assertions to make sure the end state is good // 1. The proper head is set const rpc = new Eth1Provider({DEPOSIT_CONTRACT_ADDRESS: ZERO_HASH}, {providerUrls: [engineApiUrl], jwtSecretHex}); @@ -439,7 +445,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { } if (TX_SCENARIOS.includes("simple")) { - const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + const balance = await getBalance(jsonRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); // 0x12309ce54 = 2 * 0x9184e72a if (balance !== "0x12309ce54") throw Error("Invalid Balance"); } diff --git a/packages/lodestar/test/unit/chain/beaconProposerCache.ts b/packages/lodestar/test/unit/chain/beaconProposerCache.ts new file mode 100644 index 000000000000..46987e59953c --- /dev/null +++ b/packages/lodestar/test/unit/chain/beaconProposerCache.ts @@ -0,0 +1,37 @@ +import {expect} from "chai"; +import {BeaconProposerCache} from "../../../src/chain/beaconProposerCache"; + +const defaultFeeRecipient = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +describe("BeaconProposerCache", function () { + let cache: BeaconProposerCache; + + beforeEach(function () { + // max 2 items + cache = new BeaconProposerCache({defaultFeeRecipient}); + cache.add(1, {validatorIndex: "23", feeRecipient: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}); + cache.add(3, {validatorIndex: "43", feeRecipient: "0xcccccccccccccccccccccccccccccccccccccccc"}); + }); + + it("get default", function () { + expect(cache.get("32")).to.be.equal("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + }); + + it("get what has been set", function () { + expect(cache.get("23")).to.be.equal("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + }); + + it("override and get latest", function () { + cache.add(5, {validatorIndex: "23", feeRecipient: "0xdddddddddddddddddddddddddddddddddddddddd"}); + expect(cache.get("23")).to.be.equal("0xdddddddddddddddddddddddddddddddddddddddd"); + }); + + it("prune", function () { + cache.prune(4); + + // Default for what has been pruned + expect(cache.get("23")).to.be.equal("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + + // Original for what hasn't been pruned + expect(cache.get("43")).to.be.equal("0xcccccccccccccccccccccccccccccccccccccccc"); + }); +}); 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/mocks/chain/chain.ts b/packages/lodestar/test/utils/mocks/chain/chain.ts index ca0b8a14433e..19eb4462ae80 100644 --- a/packages/lodestar/test/utils/mocks/chain/chain.ts +++ b/packages/lodestar/test/utils/mocks/chain/chain.ts @@ -7,6 +7,7 @@ import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {BeaconStateAllForks, CachedBeaconStateAllForks} from "@chainsafe/lodestar-beacon-state-transition"; import {phase0} from "@chainsafe/lodestar-beacon-state-transition"; import {CheckpointWithHex, IForkChoice, IProtoBlock, ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; +import {defaultDefaultFeeRecipient} from "@chainsafe/lodestar-validator"; import {ChainEventEmitter, IBeaconChain} from "../../../../src/chain"; import {IBeaconClock} from "../../../../src/chain/clock/interface"; @@ -38,6 +39,7 @@ import {testLogger} from "../../logger"; import {ReprocessController} from "../../../../src/chain/reprocess"; import {createCachedBeaconStateTest} from "@chainsafe/lodestar-beacon-state-transition/test/utils/state"; import {SeenAggregatedAttestations} from "../../../../src/chain/seenCache/seenAggregateAndProof"; +import {BeaconProposerCache} from "../../../../src/chain/beaconProposerCache"; /* eslint-disable @typescript-eslint/no-empty-function */ @@ -84,6 +86,8 @@ export class MockBeaconChain implements IBeaconChain { readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); readonly seenContributionAndProof = new SeenContributionAndProof(null); + readonly beaconProposerCache = new BeaconProposerCache({defaultFeeRecipient: defaultDefaultFeeRecipient}); + private state: BeaconStateAllForks; private abortController: AbortController; @@ -178,6 +182,8 @@ export class MockBeaconChain implements IBeaconChain { persistInvalidSszObject(): string | null { return null; } + + async updateBeaconProposerData(): Promise {} } function mockForkChoice(): IForkChoice { diff --git a/packages/lodestar/test/utils/node/validator.ts b/packages/lodestar/test/utils/node/validator.ts index e90752334a8b..aab7a1408c45 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, @@ -115,6 +117,7 @@ export async function getAndInitDevValidators({ useRestApi, testLoggerOpts, externalSignerUrl, + defaultFeeRecipient, }: { node: BeaconNode; validatorsPerClient: number; @@ -123,6 +126,7 @@ export async function getAndInitDevValidators({ useRestApi?: boolean; testLoggerOpts?: TestLoggerOpts; externalSignerUrl?: string; + defaultFeeRecipient?: string; }): Promise<{validators: Validator[]; secretKeys: SecretKey[]}> { const validators: Promise[] = []; const secretKeys: SecretKey[] = []; @@ -163,6 +167,7 @@ export async function getAndInitDevValidators({ slashingProtection, logger, signers, + defaultFeeRecipient, }) ); } diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts index 1400ff572864..bbf9811cd617 100644 --- a/packages/validator/src/index.ts +++ b/packages/validator/src/index.ts @@ -2,7 +2,7 @@ * @module validator */ -export {Validator, ValidatorOptions} from "./validator"; +export {Validator, ValidatorOptions, defaultDefaultFeeRecipient} from "./validator"; export {ValidatorStore, SignerType, Signer, SignerLocal, SignerRemote} from "./services/validatorStore"; export {waitForGenesis} from "./genesis"; export {getMetrics, Metrics, MetricsRegister} from "./metrics"; diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index 23854eabc878..eaf18d631efb 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -1,4 +1,4 @@ -import {BLSPubkey, Slot} from "@chainsafe/lodestar-types"; +import {BLSPubkey, Slot, bellatrix} from "@chainsafe/lodestar-types"; import {IChainForkConfig} from "@chainsafe/lodestar-config"; import {ForkName} from "@chainsafe/lodestar-params"; import {extendError, prettyBytes} from "@chainsafe/lodestar-utils"; @@ -12,6 +12,7 @@ import {Metrics} from "../metrics"; type BlockProposingServiceOpts = { graffiti?: string; + strictFeeRecipientCheck?: boolean; }; /** @@ -80,7 +81,27 @@ export class BlockProposingService { this.metrics?.blockProposingErrors.inc({error: "produce"}); throw extendError(e, "Failed to produce block"); }); - this.logger.debug("Produced block", debugLogCtx); + const blockFeeRecipient = (block.data as bellatrix.BeaconBlock).body.executionPayload?.feeRecipient; + const feeRecipient = blockFeeRecipient !== undefined ? toHexString(blockFeeRecipient) : undefined; + if (feeRecipient !== undefined) { + const expectedFeeRecipient = this.validatorStore.feeRecipientByValidatorPubkey.getOrDefault(pubkeyHex); + // In Mev Builder, the feeRecipeint could differ and rewards to the feeRecipeint + // might be included in the block transactions as indicated by the BuilderBid + // Address this appropriately in the Mev boost PR + // + // Even for engine, there isn't any clarity as of now how to proceed with the + // the divergence of feeRecipient, the argument being that the bn <> engine setup + // has implied trust and are user-agents of the same entity. + // A better approach would be to have engine also provide something akin to BuilderBid + // + // The following conversation in the interop R&D channel can provide some context + // https://discord.com/channels/595666850260713488/892088344438255616/978374892678426695 + // For now providing a strick check flag to enable disable this + if (feeRecipient !== expectedFeeRecipient && this.opts.strictFeeRecipientCheck) { + throw Error(`Invalid feeRecipient=${feeRecipient}, expected=${expectedFeeRecipient}`); + } + } + this.logger.debug("Produced block", {...debugLogCtx, feeRecipient}); this.metrics?.blocksProduced.inc(); const signedBlock = await this.validatorStore.signBlock(pubkey, block.data, slot); @@ -91,7 +112,7 @@ export class BlockProposingService { this.metrics?.blockProposingErrors.inc({error: "publish"}); throw extendError(e, "Failed to publish block"); }); - this.logger.info("Published block", {...logCtx, graffiti}); + this.logger.info("Published block", {...logCtx, graffiti, feeRecipient}); this.metrics?.blocksPublished.inc(); } catch (e) { this.logger.error("Error proposing block", logCtx, e as Error); diff --git a/packages/validator/src/services/prepareBeaconProposer.ts b/packages/validator/src/services/prepareBeaconProposer.ts new file mode 100644 index 000000000000..afea73d93d16 --- /dev/null +++ b/packages/validator/src/services/prepareBeaconProposer.ts @@ -0,0 +1,54 @@ +import {Epoch} from "@chainsafe/lodestar-types"; +import {Api, routes} from "@chainsafe/lodestar-api"; + +import {ValidatorStore} from "./validatorStore"; +import {IndicesService} from "./indices"; +import {IClock, ILoggerVc} from "../util"; +import {Metrics} from "../metrics"; + +/** + * 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 indicesService: IndicesService, + private readonly metrics: Metrics | null + ) { + clock.runEveryEpoch(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[]): routes.validator.ProposerPreparationData[] { + return indices.map((validatorIndex) => ({ + validatorIndex: validatorIndex.toString(), + feeRecipient: this.validatorStore.feeRecipientByValidatorPubkey.getOrDefault( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.indicesService.index2pubkey.get(validatorIndex)! + ), + })); + } +} diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index a0052b9dcb29..f208f6eb6f96 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -34,6 +34,7 @@ import {Interchange, InterchangeFormatVersion, ISlashingProtection} from "../sla import {PubkeyHex} from "../types"; import {externalSignerPostSignature} from "../util/externalSignerClient"; import {Metrics} from "../metrics"; +import {MapDef} from "../util/map"; export enum SignerType { Local, @@ -62,6 +63,7 @@ export type Signer = SignerLocal | SignerRemote; * Service that sets up and handles validator attester duties. */ export class ValidatorStore { + readonly feeRecipientByValidatorPubkey: MapDef; private readonly validators = new Map(); private readonly genesisValidatorsRoot: Root; @@ -70,8 +72,10 @@ export class ValidatorStore { private readonly slashingProtection: ISlashingProtection, private readonly metrics: Metrics | null, signers: Signer[], - genesis: phase0.Genesis + genesis: phase0.Genesis, + defaultFeeRecipient: string ) { + this.feeRecipientByValidatorPubkey = new MapDef(() => defaultFeeRecipient); for (const signer of signers) { this.addSigner(signer); } diff --git a/packages/validator/src/util/map.ts b/packages/validator/src/util/map.ts new file mode 100644 index 000000000000..dcfc421c3d7f --- /dev/null +++ b/packages/validator/src/util/map.ts @@ -0,0 +1,14 @@ +export class MapDef extends Map { + constructor(private readonly getDefault: () => V) { + super(); + } + + getOrDefault(key: K): V { + let value = super.get(key); + if (value === undefined) { + value = this.getDefault(); + this.set(key, value); + } + return value; + } +} diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index 7941b1b0f283..41cd0a364fd3 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -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"; @@ -19,9 +20,12 @@ import {toHexString} from "@chainsafe/ssz"; import {ValidatorEventEmitter} from "./services/emitter"; import {ValidatorStore, Signer} from "./services/validatorStore"; import {computeEpochAtSlot, getCurrentSlot} from "@chainsafe/lodestar-beacon-state-transition"; + import {PubkeyHex} from "./types"; import {Metrics} from "./metrics"; +export const defaultDefaultFeeRecipient = "0x0000000000000000000000000000000000000000"; + export type ValidatorOptions = { slashingProtection: ISlashingProtection; dbOps: IDatabaseApiOptions; @@ -30,6 +34,8 @@ export type ValidatorOptions = { logger: ILogger; afterBlockDelaySlotFraction?: number; graffiti?: string; + defaultFeeRecipient?: string; + strictFeeRecipientCheck?: boolean; }; // TODO: Extend the timeout, and let it be customizable @@ -52,6 +58,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 +68,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, defaultFeeRecipient, strictFeeRecipientCheck} = opts; const config = createIBeaconConfig(dbOps.config, genesis.genesisValidatorsRoot); const api = @@ -78,7 +85,14 @@ export class Validator { : opts.api; const clock = new Clock(config, logger, {genesisTime: Number(genesis.genesisTime)}); - const validatorStore = new ValidatorStore(config, slashingProtection, metrics, signers, genesis); + const validatorStore = new ValidatorStore( + config, + slashingProtection, + metrics, + signers, + genesis, + defaultFeeRecipient ?? defaultDefaultFeeRecipient + ); const indicesService = new IndicesService(logger, api, validatorStore, metrics); const emitter = new ValidatorEventEmitter(); const chainHeaderTracker = new ChainHeaderTracker(logger, api, emitter); @@ -86,6 +100,7 @@ export class Validator { this.blockProposingService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, metrics, { graffiti, + strictFeeRecipientCheck, }); this.attestationService = new AttestationService( @@ -111,6 +126,10 @@ export class Validator { metrics ); + this.prepareBeaconProposerService = defaultFeeRecipient + ? new PrepareBeaconProposerService(loggerVc, api, clock, validatorStore, indicesService, metrics) + : null; + this.config = config; this.logger = logger; this.api = api;