Skip to content

Commit e4dc8cc

Browse files
committed
perf: speed up valkeys generation (A-1361)
1 parent 2527c15 commit e4dc8cc

4 files changed

Lines changed: 193 additions & 98 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Wallet } from '@ethersproject/wallet';
2+
import { parentPort, workerData } from 'worker_threads';
3+
4+
type EthJsonV3WorkerData = {
5+
privateKeyHex: string;
6+
password: string;
7+
};
8+
9+
type SerializedError = {
10+
message: string;
11+
name?: string;
12+
stack?: string;
13+
};
14+
15+
function serializeError(error: unknown): SerializedError {
16+
if (error instanceof Error) {
17+
return { message: error.message, name: error.name, stack: error.stack };
18+
}
19+
return { message: String(error) };
20+
}
21+
22+
void (async () => {
23+
try {
24+
const { privateKeyHex, password } = workerData as EthJsonV3WorkerData;
25+
const json = await new Wallet(privateKeyHex).encrypt(password);
26+
parentPort?.postMessage({ json });
27+
} catch (error) {
28+
parentPort?.postMessage({ error: serializeError(error) });
29+
}
30+
})();

yarn-project/cli/src/cmds/validator_keys/shared.ts

Lines changed: 86 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { prettyPrintJSON } from '@aztec/cli/utils';
2+
import { asyncPool } from '@aztec/foundation/async-pool';
23
import { deriveBlsPrivateKey } from '@aztec/foundation/crypto/bls';
34
import { createBn254Keystore } from '@aztec/foundation/crypto/bls/bn254_keystore';
45
import { computeBn254G1PublicKeyCompressed } from '@aztec/foundation/crypto/bn254';
@@ -10,12 +11,60 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
1011
import { Wallet } from '@ethersproject/wallet';
1112
import { constants as fsConstants, mkdirSync } from 'fs';
1213
import { access, writeFile } from 'fs/promises';
13-
import { homedir } from 'os';
14+
import { availableParallelism, homedir } from 'os';
1415
import { dirname, isAbsolute, join } from 'path';
1516
import { mnemonicToAccount } from 'viem/accounts';
17+
import { Worker } from 'worker_threads';
1618

1719
import { defaultBlsPath } from './utils.js';
1820

21+
type EthJsonV3WorkerResult = { json: string } | { error: { message: string; name?: string; stack?: string } };
22+
23+
// ethers' default scrypt parameters use substantial memory, so keep worker fan-out bounded.
24+
const maxEthKeystoreWorkers = Math.max(1, Math.min(4, availableParallelism()));
25+
26+
function deserializeWorkerError(error: { message: string; name?: string; stack?: string }) {
27+
const result = new Error(error.message);
28+
result.name = error.name ?? result.name;
29+
result.stack = error.stack;
30+
return result;
31+
}
32+
33+
function encryptEthJsonV3InWorker(privateKeyHex: string, password: string): Promise<string> {
34+
return new Promise<string>((resolve, reject) => {
35+
const worker = new Worker(new URL('./eth_json_v3_worker.js', import.meta.url), {
36+
workerData: { privateKeyHex, password },
37+
});
38+
let settled = false;
39+
40+
worker.once('message', (result: EthJsonV3WorkerResult) => {
41+
settled = true;
42+
43+
if ('json' in result) {
44+
resolve(result.json);
45+
} else {
46+
reject(deserializeWorkerError(result.error));
47+
}
48+
});
49+
worker.once('error', error => {
50+
settled = true;
51+
reject(error);
52+
});
53+
worker.once('exit', code => {
54+
if (!settled) {
55+
reject(new Error(`ETH JSON V3 worker stopped with exit code ${code}`));
56+
}
57+
});
58+
});
59+
}
60+
61+
async function encryptEthJsonV3(privateKeyHex: string, password: string): Promise<string> {
62+
if (process.env.JEST_WORKER_ID !== undefined) {
63+
return await new Wallet(privateKeyHex).encrypt(password);
64+
}
65+
return await encryptEthJsonV3InWorker(privateKeyHex, password);
66+
}
67+
1968
export type ValidatorSummary = { attesterEth?: string; attesterBls?: string; publisherEth?: string[] };
2069

2170
export type BuildValidatorsInput = {
@@ -228,7 +277,7 @@ export async function writeBn254BlsKeystore(
228277
): Promise<string> {
229278
mkdirSync(outDir, { recursive: true });
230279

231-
const keystore = createBn254Keystore(password, privateKeyHex, pubkeyHex, derivationPath);
280+
const keystore = await createBn254Keystore(password, privateKeyHex, pubkeyHex, derivationPath);
232281

233282
const safeBase = fileNameBase.replace(/[^a-zA-Z0-9_-]/g, '_');
234283
const outPath = join(outDir, `keystore-${safeBase}.json`);
@@ -241,28 +290,29 @@ export async function writeBlsBn254ToFile(
241290
validators: ValidatorKeyStore[],
242291
options: { outDir: string; password: string; blsPath?: string },
243292
): Promise<void> {
244-
for (let i = 0; i < validators.length; i++) {
245-
const v = validators[i];
246-
if (!v || typeof v !== 'object' || !('attester' in v)) {
247-
continue;
248-
}
249-
const att = (v as any).attester;
293+
await Promise.all(
294+
validators.map(async (v, i) => {
295+
if (!v || typeof v !== 'object' || !('attester' in v)) {
296+
return;
297+
}
298+
const att = (v as any).attester;
250299

251-
// Shapes: { bls: <hex> } or { eth: <ethAccount>, bls?: <hex> } or plain EthAccount
252-
const blsKey: string | undefined = typeof att === 'object' && 'bls' in att ? (att as any).bls : undefined;
253-
if (!blsKey || typeof blsKey !== 'string') {
254-
continue;
255-
}
300+
// Shapes: { bls: <hex> } or { eth: <ethAccount>, bls?: <hex> } or plain EthAccount
301+
const blsKey: string | undefined = typeof att === 'object' && 'bls' in att ? (att as any).bls : undefined;
302+
if (!blsKey || typeof blsKey !== 'string') {
303+
return;
304+
}
256305

257-
const pub = await computeBlsPublicKeyCompressed(blsKey);
258-
const path = options.blsPath ?? defaultBlsPath;
259-
const fileBase = `${String(i + 1)}_${pub.slice(2, 18)}`;
260-
const keystorePath = await writeBn254BlsKeystore(options.outDir, fileBase, options.password, blsKey, pub, path);
306+
const pub = await computeBlsPublicKeyCompressed(blsKey);
307+
const path = options.blsPath ?? defaultBlsPath;
308+
const fileBase = `${String(i + 1)}_${pub.slice(2, 18)}`;
309+
const keystorePath = await writeBn254BlsKeystore(options.outDir, fileBase, options.password, blsKey, pub, path);
261310

262-
if (typeof att === 'object') {
263-
(att as any).bls = { path: keystorePath, password: options.password };
264-
}
265-
}
311+
if (typeof att === 'object') {
312+
(att as any).bls = { path: keystorePath, password: options.password };
313+
}
314+
}),
315+
);
266316
}
267317

268318
/** Writes an Ethereum JSON V3 keystore using ethers, returns absolute path */
@@ -274,8 +324,7 @@ export async function writeEthJsonV3Keystore(
274324
): Promise<string> {
275325
const safeBase = fileNameBase.replace(/[^a-zA-Z0-9_-]/g, '_');
276326
mkdirSync(outDir, { recursive: true });
277-
const wallet = new Wallet(privateKeyHex);
278-
const json = await wallet.encrypt(password);
327+
const json = await encryptEthJsonV3(privateKeyHex, password);
279328
const outPath = join(outDir, `keystore-eth-${safeBase}.json`);
280329
await writeFile(outPath, json, { encoding: 'utf-8' });
281330
return outPath;
@@ -286,13 +335,16 @@ export async function writeEthJsonV3ToFile(
286335
validators: ValidatorKeyStore[],
287336
options: { outDir: string; password: string },
288337
): Promise<void> {
289-
const maybeEncryptEth = async (account: any, label: string) => {
338+
const tasks: (() => Promise<void>)[] = [];
339+
340+
const maybeQueueEncryptEth = (account: any, label: string, setEncryptedAccount: (account: any) => void) => {
290341
if (typeof account === 'string' && account.startsWith('0x') && account.length === 66) {
291-
const fileBase = `${label}_${account.slice(2, 10)}`;
292-
const p = await writeEthJsonV3Keystore(options.outDir, fileBase, options.password, account);
293-
return { path: p, password: options.password };
342+
tasks.push(async () => {
343+
const fileBase = `${label}_${account.slice(2, 10)}`;
344+
const path = await writeEthJsonV3Keystore(options.outDir, fileBase, options.password, account);
345+
setEncryptedAccount({ path, password: options.password });
346+
});
294347
}
295-
return account;
296348
};
297349

298350
for (let i = 0; i < validators.length; i++) {
@@ -304,23 +356,23 @@ export async function writeEthJsonV3ToFile(
304356
// attester may be string (eth), object with eth, or remote signer
305357
const att = (v as any).attester;
306358
if (typeof att === 'string') {
307-
(v as any).attester = await maybeEncryptEth(att, `attester_${i + 1}`);
359+
maybeQueueEncryptEth(att, `attester_${i + 1}`, account => ((v as any).attester = account));
308360
} else if (att && typeof att === 'object' && 'eth' in att) {
309-
(att as any).eth = await maybeEncryptEth((att as any).eth, `attester_${i + 1}`);
361+
maybeQueueEncryptEth((att as any).eth, `attester_${i + 1}`, account => ((att as any).eth = account));
310362
}
311363

312364
// publisher can be single or array
313365
if ('publisher' in v) {
314366
const pub = (v as any).publisher;
315367
if (Array.isArray(pub)) {
316-
const out: any[] = [];
317368
for (let j = 0; j < pub.length; j++) {
318-
out.push(await maybeEncryptEth(pub[j], `publisher_${i + 1}_${j + 1}`));
369+
maybeQueueEncryptEth(pub[j], `publisher_${i + 1}_${j + 1}`, account => (pub[j] = account));
319370
}
320-
(v as any).publisher = out;
321371
} else if (pub !== undefined) {
322-
(v as any).publisher = await maybeEncryptEth(pub, `publisher_${i + 1}`);
372+
maybeQueueEncryptEth(pub, `publisher_${i + 1}`, account => ((v as any).publisher = account));
323373
}
324374
}
325375
}
376+
377+
await asyncPool(maxEthKeystoreWorkers, tasks, task => task());
326378
}

0 commit comments

Comments
 (0)