From ad96cfd4ae1cb2d38218bdcb869b07a3dc1c7e96 Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 19 Aug 2022 16:57:50 +0530 Subject: [PATCH] Cache and reuse old validator registation data if no params change --- .../src/services/prepareBeaconProposer.ts | 7 +-- .../validator/src/services/validatorStore.ts | 23 ++++++++ packages/validator/src/util/map.ts | 21 +++++++ .../src/util/validatorRegistrationCache.ts | 49 ++++++++++++++++ .../services/getValidatorRegistration.test.ts | 56 +++++++++++++++++++ 5 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 packages/validator/src/util/validatorRegistrationCache.ts create mode 100644 packages/validator/test/unit/services/getValidatorRegistration.test.ts diff --git a/packages/validator/src/services/prepareBeaconProposer.ts b/packages/validator/src/services/prepareBeaconProposer.ts index 91f2a3eb8fa0..eb89603589ab 100644 --- a/packages/validator/src/services/prepareBeaconProposer.ts +++ b/packages/validator/src/services/prepareBeaconProposer.ts @@ -1,7 +1,6 @@ import {Epoch, bellatrix} from "@lodestar/types"; import {Api, routes} from "@lodestar/api"; import {IBeaconConfig} from "@lodestar/config"; -import {fromHexString} from "@chainsafe/ssz"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {IClock, ILoggerVc, batchItems} from "../util/index.js"; @@ -95,11 +94,7 @@ export function pollBuilderValidatorRegistration( const pubkeyHex = validatorStore.getPubkeyOfIndex(index); if (!pubkeyHex) throw Error(`Pubkey lookup failure for index=${index}`); const feeRecipient = validatorStore.getFeeRecipient(pubkeyHex); - return validatorStore.signValidatorRegistration( - fromHexString(pubkeyHex), - fromHexString(feeRecipient), - slot - ); + return validatorStore.getValidatorRegistration(pubkeyHex, feeRecipient, slot); } ) ); diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 22dd08ff8fea..05ff620aa0c6 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -38,6 +38,7 @@ import {routes} from "@lodestar/api"; import {ISlashingProtection} from "../slashingProtection/index.js"; import {PubkeyHex} from "../types.js"; import {externalSignerPostSignature} from "../util/externalSignerClient.js"; +import {ValidatorRegistrationCache} from "../util/validatorRegistrationCache.js"; import {Metrics} from "../metrics.js"; import {IndicesService} from "./indices.js"; import {DoppelgangerService} from "./doppelgangerService.js"; @@ -79,6 +80,8 @@ type ValidatorData = { */ export class ValidatorStore { private readonly validators = new Map(); + private readonly validatorRegistrationCache = new ValidatorRegistrationCache(); + /** Initially true because there are no validators */ private pubkeysToDiscover: PubkeyHex[] = []; @@ -370,6 +373,26 @@ export class ValidatorStore { }; } + async getValidatorRegistration( + pubKey: PubkeyHex, + feeRecipient: string, + slot: Slot + ): Promise { + const gasLimit = this.gasLimit; + let validatorRegistration: bellatrix.SignedValidatorRegistrationV1 | undefined; + if ((validatorRegistration = this.validatorRegistrationCache.get({pubKey, feeRecipient, gasLimit}))) { + return validatorRegistration; + } else { + validatorRegistration = await this.signValidatorRegistration( + fromHexString(pubKey), + fromHexString(feeRecipient), + slot + ); + this.validatorRegistrationCache.add({pubKey, feeRecipient, gasLimit}, validatorRegistration); + return validatorRegistration; + } + } + private async getSignature(pubkey: BLSPubkeyMaybeHex, signingRoot: Uint8Array): Promise { // TODO: Refactor indexing to not have to run toHexString() on the pubkey every time const pubkeyHex = typeof pubkey === "string" ? pubkey : toHexString(pubkey); diff --git a/packages/validator/src/util/map.ts b/packages/validator/src/util/map.ts index dcfc421c3d7f..03308da22f74 100644 --- a/packages/validator/src/util/map.ts +++ b/packages/validator/src/util/map.ts @@ -12,3 +12,24 @@ export class MapDef extends Map { return value; } } + +/** + * Prune an arbitrary set removing the first keys to have a set.size === maxItems. + * Returns the count of deleted items. + */ +export function pruneSetToMax(set: Set | Map, maxItems: number): number { + let itemsToDelete = set.size - maxItems; + const deletedItems = Math.max(0, itemsToDelete); + + if (itemsToDelete > 0) { + for (const key of set.keys()) { + set.delete(key); + itemsToDelete--; + if (itemsToDelete <= 0) { + break; + } + } + } + + return deletedItems; +} diff --git a/packages/validator/src/util/validatorRegistrationCache.ts b/packages/validator/src/util/validatorRegistrationCache.ts new file mode 100644 index 000000000000..a1be12719135 --- /dev/null +++ b/packages/validator/src/util/validatorRegistrationCache.ts @@ -0,0 +1,49 @@ +import {bellatrix} from "@lodestar/types"; +import {PubkeyHex} from "../types.js"; +import {pruneSetToMax} from "./map.js"; + +/** Maximum number of validators that can connect to a single validator process */ +const MAX_REGISTRATION_IDS = 1_00_000; +type RegistrationKeyAttributes = { + pubKey: PubkeyHex; + feeRecipient: string; + gasLimit: number; +}; + +export class ValidatorRegistrationCache { + private readonly validatorRegistrationMap = new Map< + string, + {validatorRegistration: bellatrix.SignedValidatorRegistrationV1; fullKey: string} + >(); + + getKey({pubKey}: Pick): string { + return pubKey; + } + + getFullKey({pubKey, feeRecipient, gasLimit}: RegistrationKeyAttributes): string { + return `${pubKey}-${feeRecipient}-${gasLimit}`; + } + + add(regAttributes: RegistrationKeyAttributes, validatorRegistration: bellatrix.SignedValidatorRegistrationV1): void { + const key = this.getKey(regAttributes); + const fullKey = this.getFullKey(regAttributes); + this.validatorRegistrationMap.set(key, {validatorRegistration, fullKey}); + } + + prune(): void { + // This is not so optimized function, but could maintain a 2d array may be? + pruneSetToMax(this.validatorRegistrationMap, MAX_REGISTRATION_IDS); + } + + get(regAttributes: RegistrationKeyAttributes): bellatrix.SignedValidatorRegistrationV1 | undefined { + const key = this.getKey(regAttributes); + const fullKey = this.getFullKey(regAttributes); + const regData = this.validatorRegistrationMap.get(key); + return regData?.fullKey === fullKey ? regData.validatorRegistration : undefined; + } + + has(pubKey: PubkeyHex): boolean { + const key = this.getKey({pubKey}); + return this.validatorRegistrationMap.get(key) !== undefined; + } +} diff --git a/packages/validator/test/unit/services/getValidatorRegistration.test.ts b/packages/validator/test/unit/services/getValidatorRegistration.test.ts new file mode 100644 index 000000000000..b2bbc564f04f --- /dev/null +++ b/packages/validator/test/unit/services/getValidatorRegistration.test.ts @@ -0,0 +1,56 @@ +import {expect} from "chai"; +import sinon from "sinon"; +import bls from "@chainsafe/bls"; +import {toHexString, fromHexString} from "@chainsafe/ssz"; +import {chainConfig} from "@lodestar/config/default"; +import {ValidatorStore} from "../../../src/services/validatorStore.js"; +import {getApiClientStub} from "../../utils/apiStub.js"; +import {initValidatorStore} from "../../utils/validatorStore.js"; + +describe("getValidatorRegistration", function () { + const sandbox = sinon.createSandbox(); + const api = getApiClientStub(sandbox); + + let pubkeys: string[]; // Initialize pubkeys in before() so bls is already initialized + let validatorStore: ValidatorStore; + let signValidatorStub: sinon.SinonStub; + + before(() => { + const secretKeys = Array.from({length: 1}, (_, i) => bls.SecretKey.fromBytes(Buffer.alloc(32, i + 1))); + pubkeys = secretKeys.map((sk) => toHexString(sk.toPublicKey().toBytes())); + + validatorStore = initValidatorStore(secretKeys, api, chainConfig); + + signValidatorStub = sinon.stub(validatorStore, "signValidatorRegistration").resolves({ + message: { + feeRecipient: fromHexString("0x00"), + gasLimit: 10000, + timestamp: Date.now(), + pubkey: fromHexString(pubkeys[0]), + }, + signature: Buffer.alloc(96, 0), + }); + }); + + after(() => { + sandbox.restore(); + }); + + it("Should update cache and return from cache next time", async () => { + const slot = 0; + const val1 = validatorStore.getValidatorRegistration(pubkeys[0], "0x00", slot); + await new Promise((r) => setTimeout(r, 10)); + expect(validatorStore["validatorRegistrationCache"].has(pubkeys[0])).to.be.true; + expect(signValidatorStub.callCount).to.equal(1, "signValidatorRegistration() must be called once after 1st call"); + + const val2 = validatorStore.getValidatorRegistration(pubkeys[0], "0x00", slot); + expect(JSON.stringify(val1) === JSON.stringify(val2)); + expect(signValidatorStub.callCount).to.equal( + 1, + "signValidatorRegistration() must be called once even after 2nd call" + ); + + await validatorStore.getValidatorRegistration(pubkeys[0], "0x10", slot); + expect(signValidatorStub.callCount).to.equal(2, "signValidatorRegistration() must be called twice"); + }); +});