Skip to content

Commit ed11ab8

Browse files
committed
perf: speed up valkeys generation (A-1361)
1 parent b512406 commit ed11ab8

4 files changed

Lines changed: 180 additions & 156 deletions

File tree

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

Lines changed: 88 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { prettyPrintJSON } from '@aztec/cli/utils';
2+
import { times, timesParallel } from '@aztec/foundation/collection';
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';
@@ -91,62 +92,57 @@ export async function buildValidatorEntries(input: BuildValidatorsInput) {
9192
remoteSigner,
9293
} = input;
9394

94-
const summaries: ValidatorSummary[] = [];
95-
96-
const validators = await Promise.all(
97-
Array.from({ length: validatorCount }, async (_unused, i) => {
98-
const addressIndex = baseAddressIndex + i;
99-
const basePath = blsPath ?? defaultBlsPath;
100-
const perValidatorPath = withValidatorIndex(basePath, accountIndex, addressIndex);
101-
102-
const blsPrivKey = ikm || mnemonic ? deriveBlsPrivateKey(mnemonic, ikm, perValidatorPath) : undefined;
103-
const blsPubCompressed = blsPrivKey ? await computeBlsPublicKeyCompressed(blsPrivKey) : undefined;
104-
105-
const ethAttester = deriveEthAttester(mnemonic, accountIndex, addressIndex, remoteSigner);
106-
const attester = blsPrivKey ? { eth: ethAttester, bls: blsPrivKey } : ethAttester;
107-
108-
let publisherField: EthAccount | EthPrivateKey | (EthAccount | EthPrivateKey)[] | undefined;
109-
const publisherAddresses: string[] = [];
110-
if (publishers && publishers.length > 0) {
111-
publisherAddresses.push(...publishers);
112-
publisherField = publishers.length === 1 ? (publishers[0] as EthPrivateKey) : (publishers as EthPrivateKey[]);
113-
} else if (publisherCount > 0) {
114-
const publishersBaseIndex = baseAddressIndex + validatorCount + i * publisherCount;
115-
const publisherAccounts = Array.from({ length: publisherCount }, (_unused2, j) => {
116-
const publisherAddressIndex = publishersBaseIndex + j;
117-
const pubAcct = mnemonicToAccount(mnemonic, {
118-
accountIndex,
119-
addressIndex: publisherAddressIndex,
120-
});
121-
publisherAddresses.push(pubAcct.address as unknown as string);
122-
return remoteSigner
123-
? ({ address: pubAcct.address as unknown as EthAddress, remoteSignerUrl: remoteSigner } as EthAccount)
124-
: (('0x' + Buffer.from(pubAcct.getHdKey().privateKey!).toString('hex')) as EthPrivateKey);
95+
const entries = await timesParallel(validatorCount, async i => {
96+
const addressIndex = baseAddressIndex + i;
97+
const basePath = blsPath ?? defaultBlsPath;
98+
const perValidatorPath = withValidatorIndex(basePath, accountIndex, addressIndex);
99+
100+
const blsPrivKey = ikm || mnemonic ? deriveBlsPrivateKey(mnemonic, ikm, perValidatorPath) : undefined;
101+
const blsPubCompressed = blsPrivKey ? await computeBlsPublicKeyCompressed(blsPrivKey) : undefined;
102+
const attesterAccount = mnemonicToAccount(mnemonic, { accountIndex, addressIndex });
103+
const attesterEthAddress = attesterAccount.address as unknown as string;
104+
const ethAttester = remoteSigner
105+
? ({ address: attesterAccount.address as unknown as EthAddress, remoteSignerUrl: remoteSigner } as EthAccount)
106+
: (('0x' + Buffer.from(attesterAccount.getHdKey().privateKey!).toString('hex')) as EthPrivateKey);
107+
const attester = blsPrivKey ? { eth: ethAttester, bls: blsPrivKey } : ethAttester;
108+
109+
let publisherField: EthAccount | EthPrivateKey | (EthAccount | EthPrivateKey)[] | undefined;
110+
const publisherAddresses: string[] = [];
111+
if (publishers && publishers.length > 0) {
112+
publisherAddresses.push(...publishers);
113+
publisherField = publishers.length === 1 ? (publishers[0] as EthPrivateKey) : (publishers as EthPrivateKey[]);
114+
} else if (publisherCount > 0) {
115+
const publishersBaseIndex = baseAddressIndex + validatorCount + i * publisherCount;
116+
const publisherAccounts = times(publisherCount, j => {
117+
const publisherAddressIndex = publishersBaseIndex + j;
118+
const pubAcct = mnemonicToAccount(mnemonic, {
119+
accountIndex,
120+
addressIndex: publisherAddressIndex,
125121
});
126-
publisherField = publisherCount === 1 ? publisherAccounts[0] : publisherAccounts;
127-
}
128-
129-
const acct = mnemonicToAccount(mnemonic, {
130-
accountIndex,
131-
addressIndex,
132-
});
133-
const attesterEthAddress = acct.address as unknown as string;
134-
summaries.push({
135-
attesterEth: attesterEthAddress,
136-
attesterBls: blsPubCompressed,
137-
publisherEth: publisherAddresses.length > 0 ? publisherAddresses : undefined,
122+
publisherAddresses.push(pubAcct.address as unknown as string);
123+
return remoteSigner
124+
? ({ address: pubAcct.address as unknown as EthAddress, remoteSignerUrl: remoteSigner } as EthAccount)
125+
: (('0x' + Buffer.from(pubAcct.getHdKey().privateKey!).toString('hex')) as EthPrivateKey);
138126
});
127+
publisherField = publisherCount === 1 ? publisherAccounts[0] : publisherAccounts;
128+
}
139129

140-
return {
130+
return {
131+
validator: {
141132
attester,
142133
...(publisherField !== undefined ? { publisher: publisherField } : {}),
143134
feeRecipient,
144135
coinbase: coinbase ?? attesterEthAddress,
145-
} as ValidatorKeyStore;
146-
}),
147-
);
136+
} as ValidatorKeyStore,
137+
summary: {
138+
attesterEth: attesterEthAddress,
139+
attesterBls: blsPubCompressed,
140+
publisherEth: publisherAddresses.length > 0 ? publisherAddresses : undefined,
141+
} as ValidatorSummary,
142+
};
143+
});
148144

149-
return { validators, summaries };
145+
return { validators: entries.map(entry => entry.validator), summaries: entries.map(entry => entry.summary) };
150146
}
151147

152148
export async function resolveKeystoreOutputPath(dataDir?: string, file?: string) {
@@ -228,7 +224,7 @@ export async function writeBn254BlsKeystore(
228224
): Promise<string> {
229225
mkdirSync(outDir, { recursive: true });
230226

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

233229
const safeBase = fileNameBase.replace(/[^a-zA-Z0-9_-]/g, '_');
234230
const outPath = join(outDir, `keystore-${safeBase}.json`);
@@ -241,28 +237,29 @@ export async function writeBlsBn254ToFile(
241237
validators: ValidatorKeyStore[],
242238
options: { outDir: string; password: string; blsPath?: string },
243239
): 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;
240+
await Promise.all(
241+
validators.map(async (v, i) => {
242+
if (!v || typeof v !== 'object' || !('attester' in v)) {
243+
return;
244+
}
245+
const att = (v as any).attester;
250246

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-
}
247+
// Shapes: { bls: <hex> } or { eth: <ethAccount>, bls?: <hex> } or plain EthAccount
248+
const blsKey: string | undefined = typeof att === 'object' && 'bls' in att ? (att as any).bls : undefined;
249+
if (!blsKey || typeof blsKey !== 'string') {
250+
return;
251+
}
256252

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);
253+
const pub = await computeBlsPublicKeyCompressed(blsKey);
254+
const path = options.blsPath ?? defaultBlsPath;
255+
const fileBase = `${String(i + 1)}_${pub.slice(2, 18)}`;
256+
const keystorePath = await writeBn254BlsKeystore(options.outDir, fileBase, options.password, blsKey, pub, path);
261257

262-
if (typeof att === 'object') {
263-
(att as any).bls = { path: keystorePath, password: options.password };
264-
}
265-
}
258+
if (typeof att === 'object') {
259+
(att as any).bls = { path: keystorePath, password: options.password };
260+
}
261+
}),
262+
);
266263
}
267264

268265
/** Writes an Ethereum JSON V3 keystore using ethers, returns absolute path */
@@ -295,32 +292,31 @@ export async function writeEthJsonV3ToFile(
295292
return account;
296293
};
297294

298-
for (let i = 0; i < validators.length; i++) {
299-
const v = validators[i];
300-
if (!v || typeof v !== 'object') {
301-
continue;
302-
}
295+
await Promise.all(
296+
validators.map(async (v, i) => {
297+
if (!v || typeof v !== 'object') {
298+
return;
299+
}
303300

304-
// attester may be string (eth), object with eth, or remote signer
305-
const att = (v as any).attester;
306-
if (typeof att === 'string') {
307-
(v as any).attester = await maybeEncryptEth(att, `attester_${i + 1}`);
308-
} else if (att && typeof att === 'object' && 'eth' in att) {
309-
(att as any).eth = await maybeEncryptEth((att as any).eth, `attester_${i + 1}`);
310-
}
301+
// attester may be string (eth), object with eth, or remote signer
302+
const att = (v as any).attester;
303+
if (typeof att === 'string') {
304+
(v as any).attester = await maybeEncryptEth(att, `attester_${i + 1}`);
305+
} else if (att && typeof att === 'object' && 'eth' in att) {
306+
(att as any).eth = await maybeEncryptEth((att as any).eth, `attester_${i + 1}`);
307+
}
311308

312-
// publisher can be single or array
313-
if ('publisher' in v) {
314-
const pub = (v as any).publisher;
315-
if (Array.isArray(pub)) {
316-
const out: any[] = [];
317-
for (let j = 0; j < pub.length; j++) {
318-
out.push(await maybeEncryptEth(pub[j], `publisher_${i + 1}_${j + 1}`));
309+
// publisher can be single or array
310+
if ('publisher' in v) {
311+
const pub = (v as any).publisher;
312+
if (Array.isArray(pub)) {
313+
(v as any).publisher = await Promise.all(
314+
pub.map((account, j) => maybeEncryptEth(account, `publisher_${i + 1}_${j + 1}`)),
315+
);
316+
} else if (pub !== undefined) {
317+
(v as any).publisher = await maybeEncryptEth(pub, `publisher_${i + 1}`);
319318
}
320-
(v as any).publisher = out;
321-
} else if (pub !== undefined) {
322-
(v as any).publisher = await maybeEncryptEth(pub, `publisher_${i + 1}`);
323319
}
324-
}
325-
}
320+
}),
321+
);
326322
}

yarn-project/cli/src/cmds/validator_keys/valkeys.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,21 @@ describe('validator keys utilities', () => {
181181
expect(summaries[0].publisherEth!.length).toBe(3);
182182
});
183183

184+
it('preserves summary order for multiple validators', async () => {
185+
const { summaries } = await buildValidatorEntries({
186+
validatorCount: 3,
187+
publisherCount: 0,
188+
accountIndex: 0,
189+
baseAddressIndex: 0,
190+
mnemonic: TEST_MNEMONIC,
191+
feeRecipient: feeRecipient,
192+
});
193+
194+
expect(summaries.map(summary => summary.attesterEth)).toEqual(
195+
[0, 1, 2].map(addressIndex => mnemonicToAccount(TEST_MNEMONIC, { accountIndex: 0, addressIndex }).address),
196+
);
197+
});
198+
184199
it('derives different BLS and ETH keys when account index changes', async () => {
185200
// Build with account index 0
186201
const { validators: v1, summaries: s1 } = await buildValidatorEntries({

0 commit comments

Comments
 (0)