@aztec/cli 3.0.0-nightly.20251118 → 3.0.0-nightly.20251120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dest/cmds/validator_keys/add.d.ts.map +1 -1
  2. package/dest/cmds/validator_keys/add.js +19 -3
  3. package/dest/cmds/validator_keys/generate_bls_keypair.d.ts.map +1 -1
  4. package/dest/cmds/validator_keys/generate_bls_keypair.js +2 -1
  5. package/dest/cmds/validator_keys/index.d.ts.map +1 -1
  6. package/dest/cmds/validator_keys/index.js +4 -3
  7. package/dest/cmds/validator_keys/new.d.ts +2 -1
  8. package/dest/cmds/validator_keys/new.d.ts.map +1 -1
  9. package/dest/cmds/validator_keys/new.js +24 -26
  10. package/dest/cmds/validator_keys/shared.d.ts +2 -0
  11. package/dest/cmds/validator_keys/shared.d.ts.map +1 -1
  12. package/dest/cmds/validator_keys/shared.js +26 -8
  13. package/dest/cmds/validator_keys/staker.d.ts.map +1 -1
  14. package/dest/cmds/validator_keys/staker.js +12 -11
  15. package/dest/cmds/validator_keys/utils.d.ts +25 -0
  16. package/dest/cmds/validator_keys/utils.d.ts.map +1 -0
  17. package/dest/cmds/validator_keys/utils.js +52 -0
  18. package/dest/config/network_config.d.ts +1 -1
  19. package/dest/config/network_config.d.ts.map +1 -1
  20. package/dest/config/network_config.js +18 -4
  21. package/package.json +30 -29
  22. package/public_include_metric_prefixes.json +1 -0
  23. package/src/cmds/validator_keys/add.ts +20 -4
  24. package/src/cmds/validator_keys/generate_bls_keypair.ts +2 -1
  25. package/src/cmds/validator_keys/index.ts +33 -17
  26. package/src/cmds/validator_keys/new.ts +35 -34
  27. package/src/cmds/validator_keys/shared.ts +27 -8
  28. package/src/cmds/validator_keys/staker.ts +14 -12
  29. package/src/cmds/validator_keys/utils.ts +80 -0
  30. package/src/config/network_config.ts +25 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/cli",
3
- "version": "3.0.0-nightly.20251118",
3
+ "version": "3.0.0-nightly.20251120",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./contracts": "./dest/cmds/contracts/index.js",
@@ -71,21 +71,21 @@
71
71
  ]
72
72
  },
73
73
  "dependencies": {
74
- "@aztec/accounts": "3.0.0-nightly.20251118",
75
- "@aztec/archiver": "3.0.0-nightly.20251118",
76
- "@aztec/aztec.js": "3.0.0-nightly.20251118",
77
- "@aztec/constants": "3.0.0-nightly.20251118",
78
- "@aztec/entrypoints": "3.0.0-nightly.20251118",
79
- "@aztec/ethereum": "3.0.0-nightly.20251118",
80
- "@aztec/foundation": "3.0.0-nightly.20251118",
81
- "@aztec/l1-artifacts": "3.0.0-nightly.20251118",
82
- "@aztec/node-keystore": "3.0.0-nightly.20251118",
83
- "@aztec/node-lib": "3.0.0-nightly.20251118",
84
- "@aztec/p2p": "3.0.0-nightly.20251118",
85
- "@aztec/protocol-contracts": "3.0.0-nightly.20251118",
86
- "@aztec/stdlib": "3.0.0-nightly.20251118",
87
- "@aztec/test-wallet": "3.0.0-nightly.20251118",
88
- "@aztec/world-state": "3.0.0-nightly.20251118",
74
+ "@aztec/accounts": "3.0.0-nightly.20251120",
75
+ "@aztec/archiver": "3.0.0-nightly.20251120",
76
+ "@aztec/aztec.js": "3.0.0-nightly.20251120",
77
+ "@aztec/constants": "3.0.0-nightly.20251120",
78
+ "@aztec/entrypoints": "3.0.0-nightly.20251120",
79
+ "@aztec/ethereum": "3.0.0-nightly.20251120",
80
+ "@aztec/foundation": "3.0.0-nightly.20251120",
81
+ "@aztec/l1-artifacts": "3.0.0-nightly.20251120",
82
+ "@aztec/node-keystore": "3.0.0-nightly.20251120",
83
+ "@aztec/node-lib": "3.0.0-nightly.20251120",
84
+ "@aztec/p2p": "3.0.0-nightly.20251120",
85
+ "@aztec/protocol-contracts": "3.0.0-nightly.20251120",
86
+ "@aztec/stdlib": "3.0.0-nightly.20251120",
87
+ "@aztec/test-wallet": "3.0.0-nightly.20251120",
88
+ "@aztec/world-state": "3.0.0-nightly.20251120",
89
89
  "@ethersproject/wallet": "^5.8.0",
90
90
  "@iarna/toml": "^2.2.5",
91
91
  "@libp2p/peer-id-factory": "^3.0.4",
@@ -99,9 +99,9 @@
99
99
  "viem": "npm:@spalladino/viem@2.38.2-eip7594.0"
100
100
  },
101
101
  "devDependencies": {
102
- "@aztec/aztec-node": "3.0.0-nightly.20251118",
103
- "@aztec/kv-store": "3.0.0-nightly.20251118",
104
- "@aztec/telemetry-client": "3.0.0-nightly.20251118",
102
+ "@aztec/aztec-node": "3.0.0-nightly.20251120",
103
+ "@aztec/kv-store": "3.0.0-nightly.20251120",
104
+ "@aztec/telemetry-client": "3.0.0-nightly.20251120",
105
105
  "@jest/globals": "^30.0.0",
106
106
  "@types/jest": "^30.0.0",
107
107
  "@types/lodash.chunk": "^4.2.9",
@@ -117,20 +117,21 @@
117
117
  "typescript": "^5.3.3"
118
118
  },
119
119
  "peerDependencies": {
120
- "@aztec/accounts": "3.0.0-nightly.20251118",
121
- "@aztec/bb-prover": "3.0.0-nightly.20251118",
122
- "@aztec/ethereum": "3.0.0-nightly.20251118",
123
- "@aztec/l1-artifacts": "3.0.0-nightly.20251118",
124
- "@aztec/noir-contracts.js": "3.0.0-nightly.20251118",
125
- "@aztec/noir-protocol-circuits-types": "3.0.0-nightly.20251118",
126
- "@aztec/noir-test-contracts.js": "3.0.0-nightly.20251118",
127
- "@aztec/protocol-contracts": "3.0.0-nightly.20251118",
128
- "@aztec/stdlib": "3.0.0-nightly.20251118"
120
+ "@aztec/accounts": "3.0.0-nightly.20251120",
121
+ "@aztec/bb-prover": "3.0.0-nightly.20251120",
122
+ "@aztec/ethereum": "3.0.0-nightly.20251120",
123
+ "@aztec/l1-artifacts": "3.0.0-nightly.20251120",
124
+ "@aztec/noir-contracts.js": "3.0.0-nightly.20251120",
125
+ "@aztec/noir-protocol-circuits-types": "3.0.0-nightly.20251120",
126
+ "@aztec/noir-test-contracts.js": "3.0.0-nightly.20251120",
127
+ "@aztec/protocol-contracts": "3.0.0-nightly.20251120",
128
+ "@aztec/stdlib": "3.0.0-nightly.20251120"
129
129
  },
130
130
  "files": [
131
131
  "dest",
132
132
  "src",
133
- "!*.test.*"
133
+ "!*.test.*",
134
+ "public_include_metric_prefixes.json"
134
135
  ],
135
136
  "types": "./dest/index.d.ts",
136
137
  "engines": {
@@ -0,0 +1 @@
1
+ ["aztec.validator", "aztec.tx_collector", "aztec.mempool", "aztec.p2p.gossip.agg_", "aztec.ivc_verifier.agg_"]
@@ -16,15 +16,24 @@ import {
16
16
  writeEthJsonV3ToFile,
17
17
  writeKeystoreFile,
18
18
  } from './shared.js';
19
+ import { validateBlsPathOptions, validatePublisherOptions, validateRemoteSignerOptions } from './utils.js';
19
20
 
20
21
  export type AddValidatorKeysOptions = NewValidatorKeystoreOptions;
21
22
 
22
23
  export async function addValidatorKeys(existing: string, options: AddValidatorKeysOptions, log: LogFn) {
24
+ // validate bls-path inputs before proceeding with key generation
25
+ validateBlsPathOptions(options);
26
+ // validate publisher options
27
+ validatePublisherOptions(options);
28
+ // validate remote signer options
29
+ validateRemoteSignerOptions(options);
30
+
23
31
  const {
24
32
  dataDir,
25
33
  file,
26
34
  count,
27
35
  publisherCount = 0,
36
+ publishers,
28
37
  mnemonic,
29
38
  accountIndex = 0,
30
39
  addressIndex,
@@ -36,7 +45,7 @@ export async function addValidatorKeys(existing: string, options: AddValidatorKe
36
45
  fundingAccount: fundingAccountOpt,
37
46
  remoteSigner: remoteSignerOpt,
38
47
  password,
39
- outDir,
48
+ encryptedKeystoreDir,
40
49
  } = options;
41
50
 
42
51
  const validatorCount = typeof count === 'number' && Number.isFinite(count) && count > 0 ? Math.floor(count) : 1;
@@ -69,6 +78,7 @@ export async function addValidatorKeys(existing: string, options: AddValidatorKe
69
78
  const { validators, summaries } = await buildValidatorEntries({
70
79
  validatorCount,
71
80
  publisherCount,
81
+ publishers,
72
82
  accountIndex,
73
83
  baseAddressIndex: effectiveBaseAddressIndex,
74
84
  mnemonic: mnemonicToUse,
@@ -84,10 +94,16 @@ export async function addValidatorKeys(existing: string, options: AddValidatorKe
84
94
 
85
95
  // If password provided, write ETH JSON V3 and BLS BN254 keystores and replace plaintext
86
96
  if (password !== undefined) {
87
- const targetDir =
88
- outDir && outDir.length > 0 ? outDir : dataDir && dataDir.length > 0 ? dataDir : dirname(existing);
97
+ let targetDir: string;
98
+ if (encryptedKeystoreDir && encryptedKeystoreDir.length > 0) {
99
+ targetDir = encryptedKeystoreDir;
100
+ } else if (dataDir && dataDir.length > 0) {
101
+ targetDir = dataDir;
102
+ } else {
103
+ targetDir = dirname(existing);
104
+ }
89
105
  await writeEthJsonV3ToFile(keystore.validators, { outDir: targetDir, password });
90
- await writeBlsBn254ToFile(keystore.validators, { outDir: targetDir, password });
106
+ await writeBlsBn254ToFile(keystore.validators, { outDir: targetDir, password, blsPath });
91
107
  }
92
108
 
93
109
  let outputPath = existing;
@@ -4,6 +4,7 @@ import type { LogFn } from '@aztec/foundation/log';
4
4
  import { writeFile } from 'fs/promises';
5
5
 
6
6
  import { computeBlsPublicKeyCompressed, withValidatorIndex } from './shared.js';
7
+ import { defaultBlsPath } from './utils.js';
7
8
 
8
9
  export type GenerateBlsKeypairOptions = {
9
10
  mnemonic?: string;
@@ -17,7 +18,7 @@ export type GenerateBlsKeypairOptions = {
17
18
 
18
19
  export async function generateBlsKeypair(options: GenerateBlsKeypairOptions, log: LogFn) {
19
20
  const { mnemonic, ikm, blsPath, compressed = true, json, out } = options;
20
- const path = withValidatorIndex(blsPath ?? 'm/12381/3600/0/0/0', 0);
21
+ const path = withValidatorIndex(blsPath ?? defaultBlsPath, 0);
21
22
  const priv = deriveBlsPrivateKey(mnemonic, ikm, path);
22
23
  const pub = await computeBlsPublicKeyCompressed(priv);
23
24
  const result = { path, privateKey: priv, publicKey: pub, format: compressed ? 'compressed' : 'uncompressed' };
@@ -3,6 +3,7 @@ import type { LogFn } from '@aztec/foundation/log';
3
3
  import { Command } from 'commander';
4
4
 
5
5
  import { parseAztecAddress, parseEthereumAddress, parseHex, parseOptionalInteger } from '../../utils/commands.js';
6
+ import { defaultBlsPath } from './utils.js';
6
7
 
7
8
  export function injectCommands(program: Command, log: LogFn) {
8
9
  const group = program
@@ -17,25 +18,32 @@ export function injectCommands(program: Command, log: LogFn) {
17
18
  .option('--data-dir <path>', 'Directory to store keystore(s). Defaults to ~/.aztec/keystore')
18
19
  .option('--file <name>', 'Keystore file name. Defaults to key1.json (or keyN.json if key1.json exists)')
19
20
  .option('--count <N>', 'Number of validators to generate', parseOptionalInteger)
20
- .option('--publisher-count <N>', 'Number of publisher accounts per validator (default 1)', value =>
21
+ .option('--publisher-count <N>', 'Number of publisher accounts per validator (default 0)', value =>
21
22
  parseOptionalInteger(value, 0),
22
23
  )
24
+ .option('--publishers <privateKeys>', 'Comma-separated list of publisher private keys for all validators.', value =>
25
+ value.split(',').map((key: string) => key.trim()),
26
+ )
23
27
  .option('--mnemonic <mnemonic>', 'Mnemonic for ETH/BLS derivation')
24
28
  .option('--passphrase <str>', 'Optional passphrase for mnemonic')
25
- .option('--account-index <N>', 'Base account index for ETH derivation', parseOptionalInteger)
26
- .option('--address-index <N>', 'Base address index for ETH derivation', parseOptionalInteger)
27
- .option('--coinbase <address>', 'Coinbase ETH address to use when proposing', parseEthereumAddress)
29
+ .option('--account-index <N>', 'Base account index for ETH/BLS derivation', parseOptionalInteger)
30
+ .option('--address-index <N>', 'Base address index for ETH/BLS derivation', parseOptionalInteger)
31
+ .option(
32
+ '--coinbase <address>',
33
+ 'Coinbase ETH address to use when proposing. Defaults to attester address.',
34
+ parseEthereumAddress,
35
+ )
28
36
  .option('--funding-account <address>', 'ETH account to fund publishers', parseEthereumAddress)
29
37
  .option('--remote-signer <url>', 'Default remote signer URL for accounts in this file')
30
38
  .option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
31
- .option('--bls-path <path>', 'EIP-2334 path (default m/12381/3600/0/0/0)')
39
+ .option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
32
40
  .option(
33
41
  '--password <str>',
34
42
  'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335). Empty string allowed',
35
43
  )
36
- .option('--out-dir <dir>', 'Output directory for generated keystore file(s)')
44
+ .option('--encrypted-keystore-dir <dir>', 'Output directory for encrypted keystore file(s)')
37
45
  .option('--json', 'Echo resulting JSON to stdout')
38
- .option('--staker-output', 'Generate staker output JSON files for each attester')
46
+ .option('--staker-output', 'Generate a single staker output JSON file with an array of validator entries')
39
47
  .option('--gse-address <address>', 'GSE contract address (required with --staker-output)', parseEthereumAddress)
40
48
  .option('--l1-rpc-urls <urls>', 'L1 RPC URLs (comma-separated, required with --staker-output)', value =>
41
49
  value.split(','),
@@ -49,6 +57,7 @@ export function injectCommands(program: Command, log: LogFn) {
49
57
  .requiredOption('--fee-recipient <address>', 'Aztec address that will receive fees', parseAztecAddress)
50
58
  .action(async options => {
51
59
  const { newValidatorKeystore } = await import('./new.js');
60
+
52
61
  await newValidatorKeystore(options, log);
53
62
  });
54
63
 
@@ -57,27 +66,34 @@ export function injectCommands(program: Command, log: LogFn) {
57
66
  .summary('Augment an existing validator keystore JSON')
58
67
  .description('Adds attester/publisher/BLS entries to an existing keystore using the same flags as new')
59
68
  .argument('<existing>', 'Path to existing keystore JSON')
60
- .option('--data-dir <path>', 'Directory where keystore(s) live')
61
- .option('--file <name>', 'Override output file name')
62
- .option('--count <N>', 'Number of validators to add', parseOptionalInteger)
63
- .option('--publisher-count <N>', 'Number of publisher accounts per validator (default 1)', value =>
69
+ .option('--data-dir <path>', 'Directory where keystore(s) live. (default: ~/.aztec/keystore)')
70
+ .option('--file <name>', 'Override output file name. (default: key<N>.json)')
71
+ .option('--count <N>', 'Number of validators to add. (default: 1)', parseOptionalInteger)
72
+ .option('--publisher-count <N>', 'Number of publisher accounts per validator (default 0)', value =>
64
73
  parseOptionalInteger(value, 0),
65
74
  )
75
+ .option('--publishers <privateKeys>', 'Comma-separated list of publisher private keys for all validators.', value =>
76
+ value.split(',').map((key: string) => key.trim()),
77
+ )
66
78
  .option('--mnemonic <mnemonic>', 'Mnemonic for ETH/BLS derivation')
67
79
  .option('--passphrase <str>', 'Optional passphrase for mnemonic')
68
- .option('--account-index <N>', 'Base account index for ETH derivation', parseOptionalInteger)
69
- .option('--address-index <N>', 'Base address index for ETH derivation', parseOptionalInteger)
70
- .option('--coinbase <address>', 'Coinbase ETH address to use when proposing', parseEthereumAddress)
80
+ .option('--account-index <N>', 'Base account index for ETH/BLS derivation', parseOptionalInteger)
81
+ .option('--address-index <N>', 'Base address index for ETH/BLS derivation', parseOptionalInteger)
82
+ .option(
83
+ '--coinbase <address>',
84
+ 'Coinbase ETH address to use when proposing. Defaults to attester address.',
85
+ parseEthereumAddress,
86
+ )
71
87
  .option('--funding-account <address>', 'ETH account to fund publishers', parseEthereumAddress)
72
88
  .option('--remote-signer <url>', 'Default remote signer URL for accounts in this file')
73
89
  .option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
74
- .option('--bls-path <path>', 'EIP-2334 path (default m/12381/3600/0/0/0)')
90
+ .option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
75
91
  .option('--empty', 'Generate an empty skeleton without keys')
76
92
  .option(
77
93
  '--password <str>',
78
94
  'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335). Empty string allowed',
79
95
  )
80
- .option('--out-dir <dir>', 'Output directory for generated keystore file(s)')
96
+ .option('--encrypted-keystore-dir <dir>', 'Output directory for encrypted keystore file(s)')
81
97
  .option('--json', 'Echo resulting JSON to stdout')
82
98
  .requiredOption('--fee-recipient <address>', 'Aztec address that will receive fees', parseAztecAddress)
83
99
  .action(async (existing: string, options) => {
@@ -110,7 +126,7 @@ export function injectCommands(program: Command, log: LogFn) {
110
126
  .description('Generate a BLS keypair with convenience flags')
111
127
  .option('--mnemonic <mnemonic>', 'Mnemonic for BLS derivation')
112
128
  .option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
113
- .option('--bls-path <path>', 'EIP-2334 path (default m/12381/3600/0/0/0)')
129
+ .option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
114
130
  .option('--g2', 'Derive on G2 subgroup')
115
131
  .option('--compressed', 'Output compressed public key')
116
132
  .option('--json', 'Print JSON output to stdout')
@@ -20,12 +20,19 @@ import {
20
20
  writeKeystoreFile,
21
21
  } from './shared.js';
22
22
  import { processAttesterAccounts } from './staker.js';
23
+ import {
24
+ validateBlsPathOptions,
25
+ validatePublisherOptions,
26
+ validateRemoteSignerOptions,
27
+ validateStakerOutputOptions,
28
+ } from './utils.js';
23
29
 
24
30
  export type NewValidatorKeystoreOptions = {
25
31
  dataDir?: string;
26
32
  file?: string;
27
33
  count?: number;
28
34
  publisherCount?: number;
35
+ publishers?: string[];
29
36
  mnemonic?: string;
30
37
  passphrase?: string;
31
38
  accountIndex?: number;
@@ -34,7 +41,7 @@ export type NewValidatorKeystoreOptions = {
34
41
  ikm?: string;
35
42
  blsPath?: string;
36
43
  password?: string;
37
- outDir?: string;
44
+ encryptedKeystoreDir?: string;
38
45
  json?: boolean;
39
46
  feeRecipient: AztecAddress;
40
47
  coinbase?: EthAddress;
@@ -47,11 +54,21 @@ export type NewValidatorKeystoreOptions = {
47
54
  };
48
55
 
49
56
  export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, log: LogFn) {
57
+ // validate bls-path inputs before proceeding with key generation
58
+ validateBlsPathOptions(options);
59
+ // validate staker output options before proceeding with key generation
60
+ validateStakerOutputOptions(options);
61
+ // validate publisher options
62
+ validatePublisherOptions(options);
63
+ // validate remote signer options
64
+ validateRemoteSignerOptions(options);
65
+
50
66
  const {
51
67
  dataDir,
52
68
  file,
53
69
  count,
54
70
  publisherCount = 0,
71
+ publishers,
55
72
  json,
56
73
  coinbase,
57
74
  accountIndex = 0,
@@ -63,32 +80,13 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
63
80
  ikm,
64
81
  mnemonic: _mnemonic,
65
82
  password,
66
- outDir,
83
+ encryptedKeystoreDir,
67
84
  stakerOutput,
68
85
  gseAddress,
69
86
  l1RpcUrls,
70
87
  l1ChainId,
71
88
  } = options;
72
89
 
73
- // Validate staker output requirements
74
- if (stakerOutput) {
75
- if (!gseAddress) {
76
- throw new Error('--gse-address is required when using --staker-output');
77
- }
78
- if (!l1RpcUrls || l1RpcUrls.length === 0) {
79
- throw new Error('--l1-rpc-urls is required when using --staker-output');
80
- }
81
- if (l1ChainId === undefined) {
82
- throw new Error('--l1-chain-id is required when using --staker-output');
83
- }
84
- }
85
-
86
- if (remoteSigner && !_mnemonic) {
87
- throw new Error(
88
- 'Using --remote-signer requires a deterministic key source. Provide --mnemonic to derive keys, or omit --remote-signer to write new private keys to keystore.',
89
- );
90
- }
91
-
92
90
  const mnemonic = _mnemonic ?? generateMnemonic(wordlist);
93
91
 
94
92
  if (!_mnemonic && !json) {
@@ -101,10 +99,12 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
101
99
 
102
100
  const validatorCount = typeof count === 'number' && Number.isFinite(count) && count > 0 ? Math.floor(count) : 1;
103
101
  const { outputPath } = await resolveKeystoreOutputPath(dataDir, file);
102
+ const keystoreOutDir = dirname(outputPath);
104
103
 
105
104
  const { validators, summaries } = await buildValidatorEntries({
106
105
  validatorCount,
107
106
  publisherCount,
107
+ publishers,
108
108
  accountIndex,
109
109
  baseAddressIndex: addressIndex,
110
110
  mnemonic,
@@ -118,9 +118,10 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
118
118
 
119
119
  // If password provided, write ETH JSON V3 and BLS BN254 keystores and replace plaintext
120
120
  if (password !== undefined) {
121
- const keystoreOutDir = outDir && outDir.length > 0 ? outDir : dirname(outputPath);
122
- await writeEthJsonV3ToFile(validators, { outDir: keystoreOutDir, password });
123
- await writeBlsBn254ToFile(validators, { outDir: keystoreOutDir, password });
121
+ const encryptedKeystoreOutDir =
122
+ encryptedKeystoreDir && encryptedKeystoreDir.length > 0 ? encryptedKeystoreDir : keystoreOutDir;
123
+ await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
124
+ await writeBlsBn254ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
124
125
  }
125
126
 
126
127
  const keystore = {
@@ -140,7 +141,6 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
140
141
  });
141
142
  const gse = new GSEContract(publicClient, gseAddress);
142
143
 
143
- const keystoreOutDir = outDir && outDir.length > 0 ? outDir : dirname(outputPath);
144
144
  // Extract keystore base name without extension for unique staker output filenames
145
145
  const keystoreBaseName = basename(outputPath, '.json');
146
146
 
@@ -149,17 +149,17 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
149
149
  const validator = validators[i];
150
150
  const outputs = await processAttesterAccounts(validator.attester, gse, password);
151
151
 
152
- // Save each attester's staker output
152
+ // Collect all staker outputs
153
153
  for (let j = 0; j < outputs.length; j++) {
154
- const attesterIndex = i + 1;
155
- const stakerOutputPath = join(
156
- keystoreOutDir,
157
- `${keystoreBaseName}_attester${attesterIndex}_staker_output.json`,
158
- );
159
- await writeFile(stakerOutputPath, prettyPrintJSON(outputs[j]), 'utf-8');
160
154
  allStakerOutputs.push(outputs[j]);
161
155
  }
162
156
  }
157
+
158
+ // Write a single JSON file with all staker outputs
159
+ if (allStakerOutputs.length > 0) {
160
+ const stakerOutputPath = join(keystoreOutDir, `${keystoreBaseName}_staker_output.json`);
161
+ await writeFile(stakerOutputPath, prettyPrintJSON(allStakerOutputs), 'utf-8');
162
+ }
163
163
  }
164
164
 
165
165
  const outputData = !_mnemonic ? { ...keystore, generatedMnemonic: mnemonic } : keystore;
@@ -178,8 +178,9 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
178
178
  } else {
179
179
  log(`Wrote validator keystore to ${outputPath}`);
180
180
  if (stakerOutput && allStakerOutputs.length > 0) {
181
- const keystoreOutDir = outDir && outDir.length > 0 ? outDir : dirname(outputPath);
182
- log(`Wrote ${allStakerOutputs.length} staker output file(s) to ${keystoreOutDir}`);
181
+ const keystoreBaseName = basename(outputPath, '.json');
182
+ const stakerOutputPath = join(keystoreOutDir, `${keystoreBaseName}_staker_output.json`);
183
+ log(`Wrote staker output for ${allStakerOutputs.length} validator(s) to ${stakerOutputPath}`);
183
184
  log('');
184
185
  }
185
186
  }
@@ -13,11 +13,14 @@ import { homedir } from 'os';
13
13
  import { dirname, isAbsolute, join } from 'path';
14
14
  import { mnemonicToAccount } from 'viem/accounts';
15
15
 
16
+ import { defaultBlsPath } from './utils.js';
17
+
16
18
  export type ValidatorSummary = { attesterEth?: string; attesterBls?: string; publisherEth?: string[] };
17
19
 
18
20
  export type BuildValidatorsInput = {
19
21
  validatorCount: number;
20
22
  publisherCount?: number;
23
+ publishers?: string[];
21
24
  accountIndex: number;
22
25
  baseAddressIndex: number;
23
26
  mnemonic: string;
@@ -30,10 +33,23 @@ export type BuildValidatorsInput = {
30
33
  };
31
34
 
32
35
  export function withValidatorIndex(path: string, accountIndex: number = 0, addressIndex: number = 0) {
36
+ // NOTE: The legacy BLS CLI is to allow users who generated keys in 2.1.4 to be able to use the same command
37
+ // to re-generate their keys. In 2.1.5 we switched how we append addresses to the path so this is to maintain backwards compatibility.
38
+ const useLegacyBlsCli = ['true', '1', 'yes', 'y'].includes(process.env.LEGACY_BLS_CLI ?? '');
39
+
40
+ const defaultBlsPathParts = defaultBlsPath.split('/');
41
+
33
42
  const parts = path.split('/');
34
- if (parts.length == 6 && parts[0] === 'm' && parts[1] === '12381' && parts[2] === '3600') {
35
- parts[3] = String(accountIndex);
36
- parts[5] = String(addressIndex);
43
+ if (parts.length == defaultBlsPathParts.length && parts.every((part, index) => part === defaultBlsPathParts[index])) {
44
+ if (useLegacyBlsCli) {
45
+ // In 2.1.4, we were using address-index in parts[3] and did NOT use account-index, check lines 32 & 84
46
+ // https://github.com/AztecProtocol/aztec-packages/blob/v2.1.4/yarn-project/cli/src/cmds/validator_keys/shared.ts
47
+
48
+ parts[3] = String(addressIndex);
49
+ } else {
50
+ parts[3] = String(accountIndex);
51
+ parts[5] = String(addressIndex);
52
+ }
37
53
  return parts.join('/');
38
54
  }
39
55
  return path;
@@ -64,6 +80,7 @@ export async function buildValidatorEntries(input: BuildValidatorsInput) {
64
80
  const {
65
81
  validatorCount,
66
82
  publisherCount = 0,
83
+ publishers,
67
84
  accountIndex,
68
85
  baseAddressIndex,
69
86
  mnemonic,
@@ -75,7 +92,6 @@ export async function buildValidatorEntries(input: BuildValidatorsInput) {
75
92
  fundingAccount,
76
93
  } = input;
77
94
 
78
- const defaultBlsPath = 'm/12381/3600/0/0/0';
79
95
  const summaries: ValidatorSummary[] = [];
80
96
 
81
97
  const validators = await Promise.all(
@@ -92,7 +108,10 @@ export async function buildValidatorEntries(input: BuildValidatorsInput) {
92
108
 
93
109
  let publisherField: EthAccount | EthPrivateKey | (EthAccount | EthPrivateKey)[] | undefined;
94
110
  const publisherAddresses: string[] = [];
95
- if (publisherCount > 0) {
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) {
96
115
  const publishersBaseIndex = baseAddressIndex + validatorCount + i * publisherCount;
97
116
  const publisherAccounts = Array.from({ length: publisherCount }, (_unused2, j) => {
98
117
  const publisherAddressIndex = publishersBaseIndex + j;
@@ -123,7 +142,7 @@ export async function buildValidatorEntries(input: BuildValidatorsInput) {
123
142
  attester,
124
143
  ...(publisherField !== undefined ? { publisher: publisherField } : {}),
125
144
  feeRecipient,
126
- coinbase,
145
+ coinbase: coinbase ?? attesterEthAddress,
127
146
  fundingAccount,
128
147
  } as ValidatorKeyStore;
129
148
  }),
@@ -222,7 +241,7 @@ export async function writeBn254BlsKeystore(
222
241
  /** Replace plaintext BLS keys in validators with { path, password } pointing to BN254 keystore files. */
223
242
  export async function writeBlsBn254ToFile(
224
243
  validators: ValidatorKeyStore[],
225
- options: { outDir: string; password: string },
244
+ options: { outDir: string; password: string; blsPath?: string },
226
245
  ): Promise<void> {
227
246
  for (let i = 0; i < validators.length; i++) {
228
247
  const v = validators[i];
@@ -238,7 +257,7 @@ export async function writeBlsBn254ToFile(
238
257
  }
239
258
 
240
259
  const pub = await computeBlsPublicKeyCompressed(blsKey);
241
- const path = 'm/12381/3600/0/0/0';
260
+ const path = options.blsPath ?? defaultBlsPath;
242
261
  const fileBase = `${String(i + 1)}_${pub.slice(2, 18)}`;
243
262
  const keystorePath = await writeBn254BlsKeystore(options.outDir, fileBase, options.password, blsKey, pub, path);
244
263
 
@@ -17,6 +17,7 @@ import type {
17
17
 
18
18
  import { Wallet } from '@ethersproject/wallet';
19
19
  import { readFileSync, writeFileSync } from 'fs';
20
+ import { basename, dirname, join } from 'path';
20
21
  import { createPublicClient, fallback, http } from 'viem';
21
22
  import { privateKeyToAddress } from 'viem/accounts';
22
23
 
@@ -275,10 +276,16 @@ export async function generateStakerJson(options: StakerOptions, log: LogFn): Pr
275
276
  });
276
277
  const gse = new GSEContract(publicClient, gseAddress);
277
278
 
278
- // Process each validator
279
- for (const validator of keystore.validators) {
279
+ const keystoreBaseName = basename(from, '.json');
280
+ const outputDir = output ? output : dirname(from);
281
+
282
+ for (let i = 0; i < keystore.validators.length; i++) {
283
+ const validator = keystore.validators[i];
280
284
  const outputs = await processAttesterAccounts(validator.attester, gse, password);
281
- allOutputs.push(...outputs);
285
+
286
+ for (let j = 0; j < outputs.length; j++) {
287
+ allOutputs.push(outputs[j]);
288
+ }
282
289
  }
283
290
 
284
291
  if (allOutputs.length === 0) {
@@ -286,13 +293,8 @@ export async function generateStakerJson(options: StakerOptions, log: LogFn): Pr
286
293
  return;
287
294
  }
288
295
 
289
- const jsonOutput = prettyPrintJSON(allOutputs);
290
-
291
- // Write to file if output is specified, otherwise log to stdout
292
- if (output) {
293
- writeFileSync(output, jsonOutput, 'utf-8');
294
- log(`Wrote staking data to ${output}`);
295
- } else {
296
- log(jsonOutput);
297
- }
296
+ // Write a single JSON file with all staker outputs
297
+ const stakerOutputPath = join(outputDir, `${keystoreBaseName}_staker_output.json`);
298
+ writeFileSync(stakerOutputPath, prettyPrintJSON(allOutputs), 'utf-8');
299
+ log(`Wrote staker output for ${allOutputs.length} validator(s) to ${stakerOutputPath}`);
298
300
  }
@@ -0,0 +1,80 @@
1
+ import type { EthAddress } from '@aztec/foundation/eth-address';
2
+ import { type EthPrivateKey, ethPrivateKeySchema } from '@aztec/node-keystore';
3
+
4
+ export const defaultBlsPath = 'm/12381/3600/0/0/0';
5
+
6
+ export function validateBlsPathOptions(options: {
7
+ count?: number;
8
+ publisherCount?: number;
9
+ accountIndex?: number;
10
+ addressIndex?: number;
11
+ blsPath?: string;
12
+ ikm?: string;
13
+ }) {
14
+ if (options.blsPath && options.blsPath !== defaultBlsPath) {
15
+ if (
16
+ (options.count && options.count !== 1) ||
17
+ (options.publisherCount && options.publisherCount > 0) ||
18
+ (options.accountIndex && options.accountIndex !== 0) ||
19
+ (options.addressIndex && options.addressIndex !== 0)
20
+ ) {
21
+ throw new Error('--bls-path cannot be used with --count, --publisher-count, --account-index, or --address-index');
22
+ }
23
+ }
24
+ }
25
+
26
+ export function validateStakerOutputOptions(options: {
27
+ stakerOutput?: boolean;
28
+ gseAddress?: EthAddress;
29
+ l1RpcUrls?: string[];
30
+ l1ChainId?: number;
31
+ }) {
32
+ if (!options.stakerOutput) {
33
+ return;
34
+ }
35
+ // Required options for staker output
36
+ if (!options.gseAddress) {
37
+ throw new Error('--gse-address is required when using --staker-output');
38
+ }
39
+ if (!options.l1RpcUrls || options.l1RpcUrls.length === 0) {
40
+ throw new Error('--l1-rpc-urls is required when using --staker-output');
41
+ }
42
+
43
+ if (options.l1ChainId === undefined) {
44
+ throw new Error('--l1-chain-id is required when using --staker-output');
45
+ }
46
+ }
47
+
48
+ export function validateRemoteSignerOptions(options: { remoteSigner?: string; mnemonic?: string }) {
49
+ if (options.remoteSigner && !options.mnemonic) {
50
+ throw new Error(
51
+ 'Using --remote-signer requires a deterministic key source. Provide --mnemonic to derive keys, or omit --remote-signer to write new private keys to keystore.',
52
+ );
53
+ }
54
+ }
55
+
56
+ export function validatePublisherOptions(options: { publishers?: string[]; publisherCount?: number }) {
57
+ if (options.publisherCount && options.publisherCount > 0 && options.publishers && options.publishers.length > 0) {
58
+ throw new Error('--publishers and --publisher-count cannot be used together');
59
+ }
60
+
61
+ if (options.publishers && options.publishers.length > 0) {
62
+ // Normalize each private key by adding 0x prefix if missing
63
+ const normalizedKeys: string[] = [];
64
+ for (const key of options.publishers) {
65
+ let privateKey = key.trim();
66
+ if (!privateKey.startsWith('0x')) {
67
+ privateKey = '0x' + privateKey;
68
+ }
69
+
70
+ try {
71
+ ethPrivateKeySchema.parse(privateKey);
72
+ normalizedKeys.push(privateKey);
73
+ } catch (error) {
74
+ throw new Error(`Invalid publisher private key: ${error instanceof Error ? error.message : String(error)}`);
75
+ }
76
+ }
77
+ // Update the options with the normalized keys
78
+ options.publishers = normalizedKeys as EthPrivateKey[];
79
+ }
80
+ }