11import { prettyPrintJSON } from '@aztec/cli/utils' ;
2+ import { asyncPool } from '@aztec/foundation/async-pool' ;
23import { deriveBlsPrivateKey } from '@aztec/foundation/crypto/bls' ;
34import { createBn254Keystore } from '@aztec/foundation/crypto/bls/bn254_keystore' ;
45import { computeBn254G1PublicKeyCompressed } from '@aztec/foundation/crypto/bn254' ;
@@ -10,12 +11,60 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
1011import { Wallet } from '@ethersproject/wallet' ;
1112import { constants as fsConstants , mkdirSync } from 'fs' ;
1213import { access , writeFile } from 'fs/promises' ;
13- import { homedir } from 'os' ;
14+ import { availableParallelism , homedir } from 'os' ;
1415import { dirname , isAbsolute , join } from 'path' ;
1516import { mnemonicToAccount } from 'viem/accounts' ;
17+ import { Worker } from 'worker_threads' ;
1618
1719import { 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+
1968export type ValidatorSummary = { attesterEth ?: string ; attesterBls ?: string ; publisherEth ?: string [ ] } ;
2069
2170export 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 - z A - Z 0 - 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 - z A - Z 0 - 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