Skip to content

Commit

Permalink
Cache and reuse old validator registation data if no params change
Browse files Browse the repository at this point in the history
  • Loading branch information
g11tech committed Aug 25, 2022
1 parent 5cf6ecb commit ad96cfd
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 6 deletions.
7 changes: 1 addition & 6 deletions packages/validator/src/services/prepareBeaconProposer.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}
)
);
Expand Down
23 changes: 23 additions & 0 deletions packages/validator/src/services/validatorStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -79,6 +80,8 @@ type ValidatorData = {
*/
export class ValidatorStore {
private readonly validators = new Map<PubkeyHex, ValidatorData>();
private readonly validatorRegistrationCache = new ValidatorRegistrationCache();

/** Initially true because there are no validators */
private pubkeysToDiscover: PubkeyHex[] = [];

Expand Down Expand Up @@ -370,6 +373,26 @@ export class ValidatorStore {
};
}

async getValidatorRegistration(
pubKey: PubkeyHex,
feeRecipient: string,
slot: Slot
): Promise<bellatrix.SignedValidatorRegistrationV1> {
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<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
21 changes: 21 additions & 0 deletions packages/validator/src/util/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,24 @@ export class MapDef<K, V> extends Map<K, V> {
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<T>(set: Set<T> | Map<T, unknown>, 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;
}
49 changes: 49 additions & 0 deletions packages/validator/src/util/validatorRegistrationCache.ts
Original file line number Diff line number Diff line change
@@ -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<RegistrationKeyAttributes, "pubKey">): 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<any>;

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");
});
});

0 comments on commit ad96cfd

Please sign in to comment.