Skip to content

Commit

Permalink
Cache and reuse old validator registration data if no params change (#…
Browse files Browse the repository at this point in the history
…4447)

* Cache and reuse old validator registation data if no params change

* add some comments

* add builder data to validatordata cache itself

* remove old val reg cache

* remove extra newlines

* cleanup test file
  • Loading branch information
g11tech authored Sep 2, 2022
1 parent 05218d8 commit 47d1529
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 6 deletions.
2 changes: 1 addition & 1 deletion packages/validator/src/services/prepareBeaconProposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function pollBuilderValidatorRegistration(
}
const feeRecipient = validatorStore.getFeeRecipient(pubkeyHex);
const gasLimit = validatorStore.getGasLimit(pubkeyHex);
return validatorStore.signValidatorRegistration(pubkeyHex, {feeRecipient, gasLimit}, slot);
return validatorStore.getValidatorRegistration(pubkeyHex, {feeRecipient, gasLimit}, slot);
}
)
);
Expand Down
35 changes: 35 additions & 0 deletions packages/validator/src/services/validatorStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ export type ValidatorProposerConfig = {
defaultConfig: ProposerConfig;
};

/**
* This cache stores SignedValidatorRegistrationV1 data for a validator so that
* we do not create and send new registration objects to avoid DOSing the builder
*
* See: https://github.com/ChainSafe/lodestar/issues/4208
*/
type BuilderData = {
validatorRegistration: bellatrix.SignedValidatorRegistrationV1;
regFullKey: string;
};

/**
* Validator entity capable of producing signatures. Either:
* - local: With BLS secret key
Expand All @@ -94,6 +105,7 @@ export type Signer = SignerLocal | SignerRemote;

type ValidatorData = ProposerConfig & {
signer: Signer;
builderData?: BuilderData;
};

export const defaultOptions = {
Expand Down Expand Up @@ -433,6 +445,29 @@ export class ValidatorStore {
};
}

async getValidatorRegistration(
pubkeyMaybeHex: BLSPubkeyMaybeHex,
regAttributes: {feeRecipient: Eth1Address; gasLimit: number},
slot: Slot
): Promise<bellatrix.SignedValidatorRegistrationV1> {
const pubkeyHex = typeof pubkeyMaybeHex === "string" ? pubkeyMaybeHex : toHexString(pubkeyMaybeHex);
const {feeRecipient, gasLimit} = regAttributes;
const regFullKey = `${feeRecipient}-${gasLimit}`;
const validatorData = this.validators.get(pubkeyHex);
const builderData = validatorData?.builderData;
if (builderData?.regFullKey === regFullKey) {
return builderData.validatorRegistration;
} else {
const validatorRegistration = await this.signValidatorRegistration(pubkeyMaybeHex, regAttributes, slot);
// If pubkeyHex was actually registered, then update the regData
if (validatorData !== undefined) {
validatorData.builderData = {validatorRegistration, regFullKey};
this.validators.set(pubkeyHex, validatorData);
}
return validatorRegistration;
}
}

private async getSignature(pubkey: BLSPubkeyMaybeHex, signingRoot: Uint8Array): Promise<BLSSignature> {
// TODO: Refactor indexing to not have to run toHexString() on the pubkey every time
const pubkeyHex = typeof pubkey === "string" ? pubkey : toHexString(pubkey);
Expand Down
73 changes: 68 additions & 5 deletions packages/validator/test/unit/validatorStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ import {expect} from "chai";
import sinon from "sinon";
import {chainConfig} from "@lodestar/config/default";
import bls from "@chainsafe/bls";
import {toHexString} from "@chainsafe/ssz";
import {toHexString, fromHexString} from "@chainsafe/ssz";
import {bellatrix} from "@lodestar/types";

import {ValidatorStore} from "../../src/services/validatorStore.js";
import {getApiClientStub} from "../utils/apiStub.js";
import {initValidatorStore} from "../utils/validatorStore.js";
import {ValidatorProposerConfig} from "../../src/services/validatorStore.js";
import {SinonStubFn} from "..//utils/types.js";

describe("ValidatorStore", function () {
const sandbox = sinon.createSandbox();
const api = getApiClientStub(sandbox);

let validatorStore: ValidatorStore;

let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized
let valProposerConfig: ValidatorProposerConfig;
let signValidatorStub: SinonStubFn<ValidatorStore["signValidatorRegistration"]>;

before(() => {
const secretKeys = Array.from({length: 3}, (_, i) => bls.SecretKey.fromBytes(toBufferBE(BigInt(i + 1), 32)));
pubkeys = secretKeys.map((sk) => sk.toPublicKey().toBytes());

valProposerConfig = {
proposerConfig: {
[toHexString(pubkeys[0])]: {
Expand All @@ -46,6 +46,11 @@ describe("ValidatorStore", function () {
};

validatorStore = initValidatorStore(secretKeys, api, chainConfig, valProposerConfig);
signValidatorStub = sinon.stub(validatorStore, "signValidatorRegistration");
});

after(() => {
sandbox.restore();
});

it("Should validate graffiti,feeRecipient etc. from valProposerConfig and ValidatorStore", async function () {
Expand Down Expand Up @@ -81,4 +86,62 @@ describe("ValidatorStore", function () {
valProposerConfig.defaultConfig.builder.gasLimit
);
});

it("Should create/update builder data and return from cache next time", async () => {
let signCallCount = 0;
let slot = 0;
const testCases: [bellatrix.SignedValidatorRegistrationV1, string, number][] = [
[valRegF00G100, "0x00", 100],
[valRegF10G100, "0x10", 100],
[valRegF10G200, "0x10", 200],
];
for (const [valReg, feeRecipient, gasLimit] of testCases) {
signValidatorStub.resolves(valReg);
const val1 = await validatorStore.getValidatorRegistration(pubkeys[0], {feeRecipient, gasLimit}, slot++);
expect(JSON.stringify(val1) === JSON.stringify(valReg));
expect(signValidatorStub.callCount).to.equal(
++signCallCount,
`signValidatorRegistration() must be updated for new feeRecipient=${feeRecipient} gasLimit=${gasLimit} combo `
);
const val2 = await validatorStore.getValidatorRegistration(pubkeys[0], {feeRecipient, gasLimit}, slot++);
expect(JSON.stringify(val2) === JSON.stringify(valReg));
expect(signValidatorStub.callCount).to.equal(
signCallCount,
`signValidatorRegistration() must be updated for same feeRecipient=${feeRecipient} gasLimit=${gasLimit} combo `
);
}
});
});

const secretKeys = Array.from({length: 3}, (_, i) => bls.SecretKey.fromBytes(toBufferBE(BigInt(i + 1), 32)));
const pubkeys = secretKeys.map((sk) => sk.toPublicKey().toBytes());

const valRegF00G100 = {
message: {
feeRecipient: fromHexString("0x00"),
gasLimit: 100,
timestamp: Date.now(),
pubkey: pubkeys[0],
},
signature: Buffer.alloc(96, 0),
};

const valRegF10G100 = {
message: {
feeRecipient: fromHexString("0x10"),
gasLimit: 100,
timestamp: Date.now(),
pubkey: pubkeys[0],
},
signature: Buffer.alloc(96, 0),
};

const valRegF10G200 = {
message: {
feeRecipient: fromHexString("0x10"),
gasLimit: 200,
timestamp: Date.now(),
pubkey: pubkeys[0],
},
signature: Buffer.alloc(96, 0),
};
5 changes: 5 additions & 0 deletions packages/validator/test/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {SinonStub} from "sinon";

export type SinonStubFn<T extends (...args: any[]) => any> = T extends (...args: infer TArgs) => infer TReturnValue
? SinonStub<TArgs, TReturnValue>
: never;

0 comments on commit 47d1529

Please sign in to comment.