Skip to content

Commit

Permalink
Add and use a default fee recipient for a validator process
Browse files Browse the repository at this point in the history
  • Loading branch information
g11tech committed May 19, 2022
1 parent 2535f40 commit a0c1035
Show file tree
Hide file tree
Showing 22 changed files with 278 additions and 25 deletions.
19 changes: 19 additions & 0 deletions packages/api/src/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ssz,
UintNum64,
ValidatorIndex,
ExecutionAddress,
} from "@chainsafe/lodestar-types";
import {
RoutesData,
Expand Down Expand Up @@ -44,6 +45,11 @@ export type SyncCommitteeSubscription = {
untilEpoch: Epoch;
};

export type ProposerPreparationData = {
validatorIndex: ValidatorIndex;
feeRecipient: ExecutionAddress;
};

export type ProposerDuty = {
slot: Slot;
validatorIndex: ValidatorIndex;
Expand Down Expand Up @@ -197,6 +203,8 @@ export type Api = {
prepareBeaconCommitteeSubnet(subscriptions: BeaconCommitteeSubscription[]): Promise<void>;

prepareSyncCommitteeSubnets(subscriptions: SyncCommitteeSubscription[]): Promise<void>;

prepareBeaconProposer(proposers: ProposerPreparationData[]): Promise<void>;
};

/**
Expand All @@ -215,6 +223,7 @@ export const routesData: RoutesData<Api> = {
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 */
Expand All @@ -231,6 +240,7 @@ export type ReqTypes = {
publishContributionAndProofs: {body: unknown};
prepareBeaconCommitteeSubnet: {body: unknown};
prepareSyncCommitteeSubnets: {body: unknown};
prepareBeaconProposer: {body: unknown};
};

export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
Expand All @@ -254,6 +264,14 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
{jsonCase: "eth2"}
);

const ProposerPreparationData = new ContainerType(
{
validatorIndex: ssz.ValidatorIndex,
feeRecipient: ssz.ExecutionAddress,
},
{jsonCase: "eth2"}
);

const produceBlock: ReqSerializers<Api, ReqTypes>["produceBlock"] = {
writeReq: (slot, randaoReveal, grafitti) => ({
params: {slot},
Expand Down Expand Up @@ -330,6 +348,7 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
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),
};
}

Expand Down
4 changes: 4 additions & 0 deletions packages/api/test/unit/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/cmds/validator/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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);
Expand Down Expand Up @@ -129,6 +132,7 @@ export async function validatorHandler(args: IValidatorCliArgs & IGlobalArgs): P
signers,
graffiti,
afterBlockDelaySlotFraction: args.afterBlockDelaySlotFraction,
defaultSuggestedFeeRecipient
},
controller.signal,
metrics
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/cmds/validator/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,6 +21,8 @@ export type IValidatorCliArgs = IAccountValidatorArgs &
force: boolean;
graffiti: string;
afterBlockDelaySlotFraction?: number;
defaultSuggestedFeeRecipient?: string;

importKeystoresPath?: string[];
importKeystoresPassword?: string;
externalSignerUrl?: string;
Expand Down Expand Up @@ -67,6 +71,13 @@ export const validatorOptions: ICliCommandOptions<IValidatorCliArgs> = {
description: "Delay before publishing attestations if block comes early, as a fraction of SECONDS_PER_SLOT",
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",
Expand Down
26 changes: 25 additions & 1 deletion packages/cli/src/options/beaconNodeOptions/execution.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
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;
};

export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] {
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,
};
}

Expand All @@ -41,4 +54,15 @@ export const options: ICliCommandOptions<ExecutionEngineArgs> = {
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",
},
};
16 changes: 16 additions & 0 deletions packages/cli/src/util/feeRecipient.ts
Original file line number Diff line number Diff line change
@@ -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)(?<feeRecipientString>[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);
}
1 change: 1 addition & 0 deletions packages/cli/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from "./stripOffNewlines";
export * from "./types";
export * from "./bls";
export * from "./jwt";
export * from "./feeRecipient";
28 changes: 28 additions & 0 deletions packages/cli/test/unit/validator/options.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
}
});
6 changes: 4 additions & 2 deletions packages/lodestar/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
},
};
}
17 changes: 15 additions & 2 deletions packages/lodestar/src/chain/factory/block/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Slot,
ssz,
ExecutionAddress,
ValidatorIndex,
} from "@chainsafe/lodestar-types";
import {
CachedBeaconStateAllForks,
Expand All @@ -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<allForks.BeaconBlockBody> {
// TODO:
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 4 additions & 5 deletions packages/lodestar/src/chain/factory/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -23,21 +23,20 @@ export async function assembleBlock(
randaoReveal,
graffiti,
slot,
feeRecipient,
}: {
randaoReveal: Bytes96;
graffiti: Bytes32;
slot: Slot;
feeRecipient: ExecutionAddress;
}
): Promise<allForks.BeaconBlock> {
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, {
Expand All @@ -46,7 +45,7 @@ export async function assembleBlock(
blockSlot: slot,
parentSlot: slot - 1,
parentBlockRoot,
feeRecipient,
proposerIndex,
}),
};

Expand Down
11 changes: 11 additions & 0 deletions packages/lodestar/src/executionEngine/disabled.ts
Original file line number Diff line number Diff line change
@@ -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<ValidatorIndex, {epoch: Epoch; feeRecipient: ExecutionAddress}>(() => ({
epoch: 0,
feeRecipient: Buffer.alloc(20, 0),
}));

async notifyNewPayload(): Promise<never> {
throw Error("Execution engine disabled");
}
Expand All @@ -12,4 +19,8 @@ export class ExecutionEngineDisabled implements IExecutionEngine {
async getPayload(): Promise<never> {
throw Error("Execution engine disabled");
}

async updateProposerPreparation(): Promise<never> {
throw Error("Execution engine disabled");
}
}
Loading

0 comments on commit a0c1035

Please sign in to comment.