Skip to content

Commit 0909b7f

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

3 files changed

Lines changed: 121 additions & 108 deletions

File tree

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

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export async function writeBn254BlsKeystore(
228228
): Promise<string> {
229229
mkdirSync(outDir, { recursive: true });
230230

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

233233
const safeBase = fileNameBase.replace(/[^a-zA-Z0-9_-]/g, '_');
234234
const outPath = join(outDir, `keystore-${safeBase}.json`);
@@ -241,28 +241,29 @@ export async function writeBlsBn254ToFile(
241241
validators: ValidatorKeyStore[],
242242
options: { outDir: string; password: string; blsPath?: string },
243243
): 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;
244+
await Promise.all(
245+
validators.map(async (v, i) => {
246+
if (!v || typeof v !== 'object' || !('attester' in v)) {
247+
return;
248+
}
249+
const att = (v as any).attester;
250250

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-
}
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+
return;
255+
}
256256

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);
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);
261261

262-
if (typeof att === 'object') {
263-
(att as any).bls = { path: keystorePath, password: options.password };
264-
}
265-
}
262+
if (typeof att === 'object') {
263+
(att as any).bls = { path: keystorePath, password: options.password };
264+
}
265+
}),
266+
);
266267
}
267268

268269
/** Writes an Ethereum JSON V3 keystore using ethers, returns absolute path */
@@ -295,32 +296,31 @@ export async function writeEthJsonV3ToFile(
295296
return account;
296297
};
297298

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

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-
}
305+
// attester may be string (eth), object with eth, or remote signer
306+
const att = (v as any).attester;
307+
if (typeof att === 'string') {
308+
(v as any).attester = await maybeEncryptEth(att, `attester_${i + 1}`);
309+
} else if (att && typeof att === 'object' && 'eth' in att) {
310+
(att as any).eth = await maybeEncryptEth((att as any).eth, `attester_${i + 1}`);
311+
}
311312

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}`));
313+
// publisher can be single or array
314+
if ('publisher' in v) {
315+
const pub = (v as any).publisher;
316+
if (Array.isArray(pub)) {
317+
(v as any).publisher = await Promise.all(
318+
pub.map((account, j) => maybeEncryptEth(account, `publisher_${i + 1}_${j + 1}`)),
319+
);
320+
} else if (pub !== undefined) {
321+
(v as any).publisher = await maybeEncryptEth(pub, `publisher_${i + 1}`);
319322
}
320-
(v as any).publisher = out;
321-
} else if (pub !== undefined) {
322-
(v as any).publisher = await maybeEncryptEth(pub, `publisher_${i + 1}`);
323323
}
324-
}
325-
}
324+
}),
325+
);
326326
}

yarn-project/foundation/src/crypto/bls/bn254_keystore.test.ts

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ describe('BN254 Keystore', () => {
2828
});
2929

3030
describe('createBn254Keystore', () => {
31-
it('creates a valid BN254 keystore structure', () => {
32-
const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
31+
it('creates a valid BN254 keystore structure', async () => {
32+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
3333

3434
expect(keystore.version).toBe(4);
3535
expect(keystore.path).toBe(testPath);
@@ -50,8 +50,8 @@ describe('BN254 Keystore', () => {
5050
expect(keystore.crypto.checksum.message).toMatch(/^[0-9a-f]{64}$/);
5151
});
5252

53-
it('encrypts the private key so it can be decrypted', () => {
54-
const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
53+
it('encrypts the private key so it can be decrypted', async () => {
54+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
5555

5656
// Derive the decryption key using the same KDF
5757
const salt = Buffer.from(keystore.crypto.kdf.params.salt, 'hex');
@@ -74,87 +74,87 @@ describe('BN254 Keystore', () => {
7474
expect('0x' + decrypted.toString('hex')).toBe(testPrivateKey);
7575
});
7676

77-
it('produces different ciphertexts for the same key with different passwords', () => {
78-
const keystore1 = createBn254Keystore('password1', testPrivateKey, testPublicKey, testPath);
79-
const keystore2 = createBn254Keystore('password2', testPrivateKey, testPublicKey, testPath);
77+
it('produces different ciphertexts for the same key with different passwords', async () => {
78+
const keystore1 = await createBn254Keystore('password1', testPrivateKey, testPublicKey, testPath);
79+
const keystore2 = await createBn254Keystore('password2', testPrivateKey, testPublicKey, testPath);
8080

8181
expect(keystore1.crypto.cipher.message).not.toBe(keystore2.crypto.cipher.message);
8282
expect(keystore1.crypto.checksum.message).not.toBe(keystore2.crypto.checksum.message);
8383
});
8484

85-
it('produces different ciphertexts on each call due to random salt and IV', () => {
86-
const keystore1 = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
87-
const keystore2 = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
85+
it('produces different ciphertexts on each call due to random salt and IV', async () => {
86+
const keystore1 = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
87+
const keystore2 = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
8888

8989
expect(keystore1.crypto.kdf.params.salt).not.toBe(keystore2.crypto.kdf.params.salt);
9090
expect(keystore1.crypto.cipher.params.iv).not.toBe(keystore2.crypto.cipher.params.iv);
9191
expect(keystore1.crypto.cipher.message).not.toBe(keystore2.crypto.cipher.message);
9292
});
9393

94-
it('accepts private keys with or without 0x prefix', () => {
95-
const withPrefix = createBn254Keystore(testPassword, '0x' + '42'.repeat(32), testPublicKey, testPath);
96-
const withoutPrefix = createBn254Keystore(testPassword, '42'.repeat(32), testPublicKey, testPath);
94+
it('accepts private keys with or without 0x prefix', async () => {
95+
const withPrefix = await createBn254Keystore(testPassword, '0x' + '42'.repeat(32), testPublicKey, testPath);
96+
const withoutPrefix = await createBn254Keystore(testPassword, '42'.repeat(32), testPublicKey, testPath);
9797

9898
// Both should produce valid keystores with same length ciphertext
9999
expect(withPrefix.crypto.cipher.message.length).toBe(64);
100100
expect(withoutPrefix.crypto.cipher.message.length).toBe(64);
101101
});
102102

103-
it('stores public key without 0x prefix in description field', () => {
103+
it('stores public key without 0x prefix in description field', async () => {
104104
const pubkeyWithPrefix = '0x' + 'ab'.repeat(33);
105-
const keystore = createBn254Keystore(testPassword, testPrivateKey, pubkeyWithPrefix, testPath);
105+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, pubkeyWithPrefix, testPath);
106106

107107
expect(keystore.description).toBe('ab'.repeat(33));
108108
expect(keystore.pubkey).toBe(pubkeyWithPrefix);
109109
});
110110

111-
it('throws error for invalid private key length', () => {
111+
it('throws error for invalid private key length', async () => {
112112
const shortKey = '0x1234';
113-
expect(() => createBn254Keystore(testPassword, shortKey, testPublicKey, testPath)).toThrow(
113+
await expect(createBn254Keystore(testPassword, shortKey, testPublicKey, testPath)).rejects.toThrow(
114114
'BLS private key must be 32-byte hex',
115115
);
116116

117117
const longKey = '0x' + '42'.repeat(33);
118-
expect(() => createBn254Keystore(testPassword, longKey, testPublicKey, testPath)).toThrow(
118+
await expect(createBn254Keystore(testPassword, longKey, testPublicKey, testPath)).rejects.toThrow(
119119
'BLS private key must be 32-byte hex',
120120
);
121121
});
122122

123-
it('throws error for non-hex private key', () => {
123+
it('throws error for non-hex private key', async () => {
124124
const invalidKey = '0xGGGG' + '42'.repeat(30);
125-
expect(() => createBn254Keystore(testPassword, invalidKey, testPublicKey, testPath)).toThrow(
125+
await expect(createBn254Keystore(testPassword, invalidKey, testPublicKey, testPath)).rejects.toThrow(
126126
'BLS private key must be 32-byte hex',
127127
);
128128
});
129129

130-
it('normalizes password using NFKD', () => {
130+
it('normalizes password using NFKD', async () => {
131131
// Password with combining characters
132132
const password = 'café'; // é can be represented as single char or e + combining accent
133-
const keystore = createBn254Keystore(password, testPrivateKey, testPublicKey, testPath);
133+
const keystore = await createBn254Keystore(password, testPrivateKey, testPublicKey, testPath);
134134

135135
// Should successfully create keystore (normalization happens internally)
136136
expect(keystore).toBeDefined();
137137
expect(keystore.version).toBe(4);
138138
});
139139

140-
it('handles empty password', () => {
141-
const keystore = createBn254Keystore('', testPrivateKey, testPublicKey, testPath);
140+
it('handles empty password', async () => {
141+
const keystore = await createBn254Keystore('', testPrivateKey, testPublicKey, testPath);
142142

143143
expect(keystore).toBeDefined();
144144
expect(keystore.crypto.cipher.message).toMatch(/^[0-9a-f]{64}$/);
145145
});
146146

147-
it('preserves derivation path exactly as provided', () => {
147+
it('preserves derivation path exactly as provided', async () => {
148148
const customPath = 'm/12381/3600/99/88/77';
149-
const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, customPath);
149+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, customPath);
150150

151151
expect(keystore.path).toBe(customPath);
152152
});
153153
});
154154

155155
describe('loadBn254Keystore', () => {
156-
it('loads and validates a valid BN254 keystore file', () => {
157-
const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
156+
it('loads and validates a valid BN254 keystore file', async () => {
157+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
158158
const filePath = join(tempDir, 'valid-keystore.json');
159159
writeFileSync(filePath, JSON.stringify(keystore));
160160

@@ -191,8 +191,8 @@ describe('BN254 Keystore', () => {
191191
});
192192

193193
describe('decryptBn254Keystore', () => {
194-
it('successfully decrypts a keystore with correct password', () => {
195-
const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
194+
it('successfully decrypts a keystore with correct password', async () => {
195+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
196196
const filePath = join(tempDir, 'decrypt-test.json');
197197
writeFileSync(filePath, JSON.stringify(keystore));
198198

@@ -201,18 +201,18 @@ describe('BN254 Keystore', () => {
201201
expect(decrypted).toBe(testPrivateKey);
202202
});
203203

204-
it('throws on incorrect password', () => {
205-
const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
204+
it('throws on incorrect password', async () => {
205+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
206206
const filePath = join(tempDir, 'wrong-password-test.json');
207207
writeFileSync(filePath, JSON.stringify(keystore));
208208

209209
expect(() => decryptBn254Keystore(filePath, 'wrong-password')).toThrow(Bn254KeystoreError);
210210
expect(() => decryptBn254Keystore(filePath, 'wrong-password')).toThrow(/Checksum verification failed/);
211211
});
212212

213-
it('works with empty password if keystore was created with empty password', () => {
213+
it('works with empty password if keystore was created with empty password', async () => {
214214
const emptyPassword = '';
215-
const keystore = createBn254Keystore(emptyPassword, testPrivateKey, testPublicKey, testPath);
215+
const keystore = await createBn254Keystore(emptyPassword, testPrivateKey, testPublicKey, testPath);
216216
const filePath = join(tempDir, 'empty-password-test.json');
217217
writeFileSync(filePath, JSON.stringify(keystore));
218218

@@ -223,25 +223,25 @@ describe('BN254 Keystore', () => {
223223
});
224224

225225
describe('decryptBn254KeystoreFromObject', () => {
226-
it('decrypts from an in-memory keystore object', () => {
227-
const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
226+
it('decrypts from an in-memory keystore object', async () => {
227+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
228228

229229
const decrypted = decryptBn254KeystoreFromObject(keystore, testPassword);
230230

231231
expect(decrypted).toBe(testPrivateKey);
232232
});
233233

234-
it('throws on unsupported KDF function', () => {
235-
const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
234+
it('throws on unsupported KDF function', async () => {
235+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
236236
// Manually modify to unsupported KDF
237237
(keystore.crypto.kdf as any).function = 'scrypt';
238238

239239
expect(() => decryptBn254KeystoreFromObject(keystore, testPassword)).toThrow(Bn254KeystoreError);
240240
expect(() => decryptBn254KeystoreFromObject(keystore, testPassword)).toThrow(/Unsupported KDF function/);
241241
});
242242

243-
it('throws on unsupported cipher function', () => {
244-
const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
243+
it('throws on unsupported cipher function', async () => {
244+
const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath);
245245
// Manually modify to unsupported cipher
246246
(keystore.crypto.cipher as any).function = 'aes-256-gcm';
247247

@@ -251,30 +251,30 @@ describe('BN254 Keystore', () => {
251251
});
252252

253253
describe('round-trip encryption and decryption', () => {
254-
it('encrypts and then decrypts to get original key', () => {
254+
it('encrypts and then decrypts to get original key', async () => {
255255
const originalKey = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
256256
const password = 'super-secret';
257257
const pubkey = '0x' + 'ff'.repeat(33);
258258
const path = 'm/12381/3600/5/0/0';
259259

260260
// Create encrypted keystore
261-
const encrypted = createBn254Keystore(password, originalKey, pubkey, path);
261+
const encrypted = await createBn254Keystore(password, originalKey, pubkey, path);
262262

263263
// Decrypt it
264264
const decrypted = decryptBn254KeystoreFromObject(encrypted, password);
265265

266266
expect(decrypted).toBe(originalKey);
267267
});
268268

269-
it('round-trips multiple different keys', () => {
269+
it('round-trips multiple different keys', async () => {
270270
const testCases = [
271271
{ key: '0x' + '11'.repeat(32), password: 'pass1' },
272272
{ key: '0x' + '22'.repeat(32), password: 'pass2' },
273273
{ key: '0x' + 'ab'.repeat(32), password: 'pass3' },
274274
];
275275

276276
for (const { key, password } of testCases) {
277-
const encrypted = createBn254Keystore(password, key, testPublicKey, testPath);
277+
const encrypted = await createBn254Keystore(password, key, testPublicKey, testPath);
278278
const decrypted = decryptBn254KeystoreFromObject(encrypted, password);
279279
expect(decrypted).toBe(key);
280280
}

0 commit comments

Comments
 (0)