@aztec-labs/cli 6.0.0-rc.3 → 6.0.0

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 (89) hide show
  1. package/dest/cmds/infrastructure/index.d.ts +1 -1
  2. package/dest/cmds/infrastructure/index.d.ts.map +1 -1
  3. package/dest/cmds/infrastructure/index.js +3 -2
  4. package/dest/cmds/infrastructure/sequencers.d.ts +4 -1
  5. package/dest/cmds/infrastructure/sequencers.d.ts.map +1 -1
  6. package/dest/cmds/infrastructure/sequencers.js +10 -3
  7. package/dest/cmds/l1/deploy_l1_contracts_cmd.d.ts +1 -1
  8. package/dest/cmds/l1/deploy_l1_contracts_cmd.d.ts.map +1 -1
  9. package/dest/cmds/l1/deploy_l1_contracts_cmd.js +1 -0
  10. package/dest/cmds/l1/index.d.ts +1 -1
  11. package/dest/cmds/l1/index.d.ts.map +1 -1
  12. package/dest/cmds/l1/index.js +2 -1
  13. package/dest/cmds/l1/l1_rpc_test_utils.d.ts +10 -2
  14. package/dest/cmds/l1/l1_rpc_test_utils.d.ts.map +1 -1
  15. package/dest/cmds/l1/l1_rpc_test_utils.js +35 -1
  16. package/dest/cmds/l1/prover_stats.d.ts +1 -1
  17. package/dest/cmds/l1/prover_stats.d.ts.map +1 -1
  18. package/dest/cmds/l1/prover_stats.js +3 -4
  19. package/dest/cmds/l1/update_l1_validators.d.ts +3 -2
  20. package/dest/cmds/l1/update_l1_validators.d.ts.map +1 -1
  21. package/dest/cmds/l1/update_l1_validators.js +6 -4
  22. package/dest/cmds/misc/generate_p2p_private_key.d.ts +1 -1
  23. package/dest/cmds/misc/generate_p2p_private_key.d.ts.map +1 -1
  24. package/dest/cmds/misc/generate_p2p_private_key.js +1 -0
  25. package/dest/cmds/validator_keys/add.d.ts +1 -1
  26. package/dest/cmds/validator_keys/add.d.ts.map +1 -1
  27. package/dest/cmds/validator_keys/add.js +14 -4
  28. package/dest/cmds/validator_keys/bls_key_derivation.d.ts +89 -0
  29. package/dest/cmds/validator_keys/bls_key_derivation.d.ts.map +1 -0
  30. package/dest/cmds/validator_keys/bls_key_derivation.js +122 -0
  31. package/dest/cmds/validator_keys/generate_bls_keypair.d.ts +3 -2
  32. package/dest/cmds/validator_keys/generate_bls_keypair.d.ts.map +1 -1
  33. package/dest/cmds/validator_keys/generate_bls_keypair.js +16 -3
  34. package/dest/cmds/validator_keys/index.d.ts +1 -1
  35. package/dest/cmds/validator_keys/index.d.ts.map +1 -1
  36. package/dest/cmds/validator_keys/index.js +14 -3
  37. package/dest/cmds/validator_keys/new.d.ts +3 -2
  38. package/dest/cmds/validator_keys/new.d.ts.map +1 -1
  39. package/dest/cmds/validator_keys/new.js +47 -33
  40. package/dest/cmds/validator_keys/shared.d.ts +28 -3
  41. package/dest/cmds/validator_keys/shared.d.ts.map +1 -1
  42. package/dest/cmds/validator_keys/shared.js +48 -7
  43. package/dest/cmds/validator_keys/staker.d.ts +6 -1
  44. package/dest/cmds/validator_keys/staker.d.ts.map +1 -1
  45. package/dest/cmds/validator_keys/staker.js +32 -0
  46. package/dest/config/chain_l2_config.d.ts +1 -1
  47. package/dest/config/chain_l2_config.d.ts.map +1 -1
  48. package/dest/config/chain_l2_config.js +1 -1
  49. package/dest/config/enrich_env.d.ts +1 -1
  50. package/dest/config/enrich_env.d.ts.map +1 -1
  51. package/dest/config/enrich_env.js +1 -1
  52. package/dest/config/get_l1_config.d.ts +1 -1
  53. package/dest/config/get_l1_config.d.ts.map +1 -1
  54. package/dest/config/get_l1_config.js +2 -1
  55. package/dest/config/network_config.d.ts +1 -1
  56. package/dest/config/network_config.d.ts.map +1 -1
  57. package/dest/config/network_config.js +4 -2
  58. package/dest/utils/aztec.d.ts +1 -1
  59. package/dest/utils/aztec.d.ts.map +1 -1
  60. package/dest/utils/aztec.js +1 -0
  61. package/dest/utils/commands.d.ts +1 -1
  62. package/dest/utils/commands.d.ts.map +1 -1
  63. package/dest/utils/commands.js +3 -1
  64. package/dest/utils/encoding.d.ts +1 -1
  65. package/dest/utils/encoding.d.ts.map +1 -1
  66. package/dest/utils/encoding.js +1 -0
  67. package/package.json +25 -25
  68. package/src/cmds/infrastructure/index.ts +13 -1
  69. package/src/cmds/infrastructure/sequencers.ts +14 -4
  70. package/src/cmds/l1/deploy_l1_contracts_cmd.ts +1 -0
  71. package/src/cmds/l1/index.ts +6 -0
  72. package/src/cmds/l1/l1_rpc_test_utils.ts +25 -2
  73. package/src/cmds/l1/prover_stats.ts +3 -2
  74. package/src/cmds/l1/update_l1_validators.ts +10 -4
  75. package/src/cmds/misc/generate_p2p_private_key.ts +1 -0
  76. package/src/cmds/validator_keys/add.ts +15 -2
  77. package/src/cmds/validator_keys/bls_key_derivation.ts +209 -0
  78. package/src/cmds/validator_keys/generate_bls_keypair.ts +26 -5
  79. package/src/cmds/validator_keys/index.ts +32 -0
  80. package/src/cmds/validator_keys/new.ts +50 -32
  81. package/src/cmds/validator_keys/shared.ts +70 -8
  82. package/src/cmds/validator_keys/staker.ts +56 -0
  83. package/src/config/chain_l2_config.ts +1 -0
  84. package/src/config/enrich_env.ts +1 -0
  85. package/src/config/get_l1_config.ts +2 -1
  86. package/src/config/network_config.ts +4 -2
  87. package/src/utils/aztec.ts +1 -0
  88. package/src/utils/commands.ts +3 -1
  89. package/src/utils/encoding.ts +1 -0
@@ -5,6 +5,7 @@ import { createAztecNodeClient } from '@aztec-labs/aztec.js/node';
5
5
  import { createEthereumChain } from '@aztec-labs/ethereum/chain';
6
6
  import { getL1ContractsConfigEnvVars } from '@aztec-labs/ethereum/config';
7
7
  import { GSEContract, RollupContract } from '@aztec-labs/ethereum/contracts';
8
+ import type { EthAddress } from '@aztec-labs/foundation/eth-address';
8
9
  import type { LogFn } from '@aztec-labs/foundation/log';
9
10
  import { createPublicClient, createWalletClient, fallback, getContract, http } from 'viem';
10
11
  import { mnemonicToAccount } from 'viem/accounts';
@@ -12,6 +13,8 @@ import { mnemonicToAccount } from 'viem/accounts';
12
13
  export async function sequencers(opts: {
13
14
  command: 'list' | 'add' | 'remove' | 'who-next' | 'flush';
14
15
  who?: string;
16
+ /** Address that receives the withdrawn stake. Required for `remove`. */
17
+ recipient?: EthAddress;
15
18
  mnemonic?: string;
16
19
  bn254SecretKey?: bigint;
17
20
  nodeUrl: string;
@@ -19,7 +22,11 @@ export async function sequencers(opts: {
19
22
  chainId: number;
20
23
  log: LogFn;
21
24
  }) {
22
- const { command, who: maybeWho, mnemonic, bn254SecretKey, nodeUrl, l1RpcUrls, chainId, log } = opts;
25
+ const { command, who: maybeWho, recipient, mnemonic, bn254SecretKey, nodeUrl, l1RpcUrls, chainId, log } = opts;
26
+ const missingRecipient = 'Missing --recipient: the address that receives the withdrawn stake';
27
+ if (command === 'remove' && !recipient) {
28
+ throw new Error(missingRecipient);
29
+ }
23
30
  const client = createAztecNodeClient(nodeUrl);
24
31
  const { l1ContractAddresses } = await client.getNodeInfo();
25
32
 
@@ -47,7 +54,7 @@ export async function sequencers(opts: {
47
54
  })
48
55
  : undefined;
49
56
 
50
- const who = (maybeWho as `0x{string}`) ?? walletClient?.account.address.toString();
57
+ const who = (maybeWho as `0x${string}`) ?? walletClient?.account.address.toString();
51
58
 
52
59
  if (command === 'list') {
53
60
  const sequencers = await rollup.getAttesters();
@@ -109,8 +116,11 @@ export async function sequencers(opts: {
109
116
  if (!who || !writeableRollup) {
110
117
  throw new Error(`Missing sequencer address`);
111
118
  }
112
- log(`Removing ${who} as sequencer`);
113
- const hash = await writeableRollup.write.initiateWithdraw([who, who]);
119
+ if (!recipient) {
120
+ throw new Error(missingRecipient);
121
+ }
122
+ log(`Removing ${who} as sequencer, withdrawing its stake to ${recipient}`);
123
+ const hash = await writeableRollup.write.initiateWithdraw([who, recipient.toString()]);
114
124
  await publicClient.waitForTransactionReceipt({ hash });
115
125
  log(`Removed in tx ${hash}`);
116
126
  } else if (command === 'who-next') {
@@ -7,6 +7,7 @@ import { SecretValue } from '@aztec-labs/foundation/config';
7
7
  import type { LogFn, Logger } from '@aztec-labs/foundation/log';
8
8
  import { protocolContractsHash } from '@aztec-labs/protocol-contracts';
9
9
  import { getGenesisValues } from '@aztec-labs/world-state/testing';
10
+ import { Buffer } from 'buffer';
10
11
  import { mnemonicToAccount } from 'viem/accounts';
11
12
 
12
13
  import { addLeadingHex } from '../../utils/aztec.js';
@@ -489,6 +489,11 @@ Note: DEADLINE is a future Unix timestamp in seconds. Run append commands sequen
489
489
  )
490
490
  .addOption(l1ChainIdOption)
491
491
  .option('--validator <address>', 'ethereum address of the validator', parseEthereumAddress)
492
+ .requiredOption(
493
+ '--recipient <address>',
494
+ 'Address that receives the withdrawn stake; it cannot be changed once set',
495
+ parseEthereumAddress,
496
+ )
492
497
  .option('--rollup <address>', 'ethereum address of the rollup contract', parseEthereumAddress)
493
498
  .action(async options => {
494
499
  const { removeL1Validator } = await import('./update_l1_validators.js');
@@ -498,6 +503,7 @@ Note: DEADLINE is a future Unix timestamp in seconds. Run append commands sequen
498
503
  privateKey: options.privateKey,
499
504
  mnemonic: options.mnemonic,
500
505
  validatorAddress: options.validator,
506
+ recipientAddress: options.recipient,
501
507
  rollupAddress: options.rollup,
502
508
  log,
503
509
  debugLogger,
@@ -1,6 +1,10 @@
1
- import type { EthAddress } from '@aztec-labs/foundation/eth-address';
1
+ import { RollupAbi } from '@aztec-foundation/l1-artifacts';
2
+
3
+ import { getPublicClient } from '@aztec-labs/ethereum/client';
4
+ import { EthAddress } from '@aztec-labs/foundation/eth-address';
2
5
  import { createServer } from 'node:http';
3
- import { encodeAbiParameters } from 'viem';
6
+ import { decodeFunctionData, encodeAbiParameters } from 'viem';
7
+ import { foundry } from 'viem/chains';
4
8
 
5
9
  /**
6
10
  * Starts a JSON-RPC server that reports `chainId` and, when `canonicalRollup` is given, answers every `eth_call` with
@@ -41,3 +45,22 @@ export async function serveL1Rpc(chainId: number, canonicalRollup?: EthAddress)
41
45
  close: () => new Promise(resolve => server.close(resolve)),
42
46
  };
43
47
  }
48
+
49
+ /**
50
+ * Returns the `(attester, recipient)` arguments of the `initiateWithdraw` call that the latest block on an anvil chain
51
+ * sent to `rollup`, failing if that block holds no such call.
52
+ */
53
+ export async function getLatestInitiateWithdrawArgs(rpcUrl: string, rollup: EthAddress) {
54
+ const client = getPublicClient({ l1RpcUrls: [rpcUrl], l1ChainId: foundry.id });
55
+ const block = await client.getBlock({ includeTransactions: true });
56
+ const tx = block.transactions.find(({ to }) => to !== null && rollup.equals(EthAddress.fromString(to)));
57
+ if (!tx) {
58
+ throw new Error(`No transaction to ${rollup} in the latest block`);
59
+ }
60
+ const decoded = decodeFunctionData({ abi: RollupAbi, data: tx.input });
61
+ if (decoded.functionName !== 'initiateWithdraw') {
62
+ throw new Error(`Expected initiateWithdraw, got ${decoded.functionName}`);
63
+ }
64
+ const [attester, recipient] = decoded.args;
65
+ return { attester: EthAddress.fromString(attester), recipient: EthAddress.fromString(recipient) };
66
+ }
@@ -2,6 +2,7 @@ import { RollupAbi } from '@aztec-foundation/l1-artifacts';
2
2
 
3
3
  import { retrieveL2ProofVerifiedEvents } from '@aztec-labs/archiver';
4
4
  import { createEthereumChain } from '@aztec-labs/ethereum/chain';
5
+ import { makeL1HttpTransport } from '@aztec-labs/ethereum/client';
5
6
  import type { ViemPublicClient } from '@aztec-labs/ethereum/types';
6
7
  import { compactArray, mapValues, unique } from '@aztec-labs/foundation/collection';
7
8
  import { EthAddress } from '@aztec-labs/foundation/eth-address';
@@ -9,7 +10,7 @@ import { type LogFn, type Logger, createLogger } from '@aztec-labs/foundation/lo
9
10
  import { createAztecNodeClient } from '@aztec-labs/stdlib/interfaces/client';
10
11
  import chunk from 'lodash.chunk';
11
12
  import groupBy from 'lodash.groupby';
12
- import { createPublicClient, fallback, getAbiItem, getAddress, http } from 'viem';
13
+ import { createPublicClient, getAbiItem, getAddress } from 'viem';
13
14
 
14
15
  export async function proverStats(opts: {
15
16
  l1RpcUrls: string[];
@@ -49,7 +50,7 @@ export async function proverStats(opts: {
49
50
  const chain = createEthereumChain(l1RpcUrls, chainId).chainInfo;
50
51
  const publicClient = createPublicClient({
51
52
  chain,
52
- transport: fallback(l1RpcUrls.map(url => http(url, { batch: false }))),
53
+ transport: makeL1HttpTransport(l1RpcUrls),
53
54
  });
54
55
  const lastBlockNum = endBlock ?? (await publicClient.getBlockNumber());
55
56
  debugLog.verbose(`Querying events on rollup at ${rollup.toString()} from ${startBlock} up to ${lastBlockNum}`);
@@ -12,6 +12,7 @@ import {
12
12
  import { createL1TxUtils } from '@aztec-labs/ethereum/l1-tx-utils';
13
13
  import { EthCheatCodes } from '@aztec-labs/ethereum/test';
14
14
  import { getActiveNetworkName } from '@aztec-labs/foundation/config';
15
+ import { getEnv } from '@aztec-labs/foundation/config/env';
15
16
  import { EthAddress } from '@aztec-labs/foundation/eth-address';
16
17
  import { Signature } from '@aztec-labs/foundation/eth-signature';
17
18
  import type { LogFn, Logger } from '@aztec-labs/foundation/log';
@@ -253,23 +254,27 @@ export async function removeL1Validator({
253
254
  privateKey,
254
255
  mnemonic,
255
256
  validatorAddress,
257
+ recipientAddress,
256
258
  rollupAddress,
257
259
  log,
258
260
  debugLogger,
259
- }: RollupCommandArgs & LoggerArgs & { validatorAddress: EthAddress }) {
261
+ }: RollupCommandArgs & LoggerArgs & { validatorAddress: EthAddress; recipientAddress: EthAddress }) {
260
262
  const dualLog = makeDualLog(log, debugLogger);
261
263
  const account = getAccount(privateKey, mnemonic);
262
264
  const chain = createEthereumChain(rpcUrls, chainId);
263
265
  const l1Client = createExtendedL1Client(rpcUrls, account, chain.chainInfo);
264
266
  const l1TxUtils = createL1TxUtils(l1Client, { logger: debugLogger });
265
267
 
266
- dualLog(`Removing validator ${validatorAddress.toString()} from rollup ${rollupAddress.toString()}`);
268
+ dualLog(
269
+ `Removing validator ${validatorAddress.toString()} from rollup ${rollupAddress.toString()}, ` +
270
+ `withdrawing its stake to ${recipientAddress.toString()}`,
271
+ );
267
272
  const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
268
273
  to: rollupAddress.toString(),
269
274
  data: encodeFunctionData({
270
275
  abi: RollupAbi,
271
276
  functionName: 'initiateWithdraw',
272
- args: [validatorAddress.toString(), validatorAddress.toString()],
277
+ args: [validatorAddress.toString(), recipientAddress.toString()],
273
278
  }),
274
279
  });
275
280
  dualLog(`Transaction hash: ${receipt.transactionHash}`);
@@ -285,7 +290,8 @@ export type AttesterExitNetwork = {
285
290
  /** Loads the published config of the network selected with --network, which takes precedence over the environment. */
286
291
  export async function getAttesterExitNetwork(name: string): Promise<AttesterExitNetwork> {
287
292
  const networkName = getActiveNetworkName(name);
288
- const cacheDir = process.env.DATA_DIRECTORY ? join(process.env.DATA_DIRECTORY, 'cache') : undefined;
293
+ const { DATA_DIRECTORY } = getEnv();
294
+ const cacheDir = DATA_DIRECTORY ? join(DATA_DIRECTORY, 'cache') : undefined;
289
295
  const config = networkName === 'local' ? undefined : await getNetworkConfig(networkName, cacheDir);
290
296
  if (networkName !== 'local' && !config) {
291
297
  throw new Error(`Network ${networkName} has no published config`);
@@ -1,5 +1,6 @@
1
1
  import type { LogFn } from '@aztec-labs/foundation/log';
2
2
  import { createSecp256k1PeerId } from '@libp2p/peer-id-factory';
3
+ import { Buffer } from 'buffer';
3
4
 
4
5
  export async function generateP2PPrivateKey(log: LogFn) {
5
6
  const peerId = await createSecp256k1PeerId();
@@ -6,9 +6,12 @@ import { wordlist } from '@scure/bip39/wordlists/english.js';
6
6
  import { dirname, isAbsolute, join } from 'path';
7
7
  import { generateMnemonic } from 'viem/accounts';
8
8
 
9
+ import { resolveBlsKeyDerivationPolicy } from './bls_key_derivation.js';
9
10
  import type { NewValidatorKeystoreOptions } from './new.js';
10
11
  import {
11
12
  buildValidatorEntries,
13
+ getBlsDerivationPaths,
14
+ getBlsKeyCandidates,
12
15
  logValidatorSummaries,
13
16
  maybePrintJson,
14
17
  writeBlsBn254ToFile,
@@ -26,6 +29,7 @@ export async function addValidatorKeys(existing: string, options: AddValidatorKe
26
29
  validatePublisherOptions(options);
27
30
  // validate remote signer options
28
31
  validateRemoteSignerOptions(options);
32
+ const blsKeyDerivation = resolveBlsKeyDerivationPolicy(options);
29
33
 
30
34
  const {
31
35
  dataDir,
@@ -83,6 +87,9 @@ export async function addValidatorKeys(existing: string, options: AddValidatorKe
83
87
  feeRecipient,
84
88
  coinbase,
85
89
  remoteSigner,
90
+ blsKeyDerivation,
91
+ // `add` never prints the mnemonic it generates.
92
+ generatedMnemonic: mnemonic ? undefined : 'hidden',
86
93
  });
87
94
 
88
95
  keystore.validators.push(...validators);
@@ -98,7 +105,11 @@ export async function addValidatorKeys(existing: string, options: AddValidatorKe
98
105
  targetDir = dirname(existing);
99
106
  }
100
107
  await writeEthJsonV3ToFile(keystore.validators, { outDir: targetDir, password });
101
- await writeBlsBn254ToFile(keystore.validators, { outDir: targetDir, password, blsPath });
108
+ await writeBlsBn254ToFile(keystore.validators, {
109
+ outDir: targetDir,
110
+ password,
111
+ derivationPaths: getBlsDerivationPaths(summaries),
112
+ });
102
113
  }
103
114
 
104
115
  let outputPath = existing;
@@ -118,5 +129,7 @@ export async function addValidatorKeys(existing: string, options: AddValidatorKe
118
129
  log(`Updated keystore ${outputPath} with ${validators.length} new validator(s)`);
119
130
  logValidatorSummaries(log, summaries);
120
131
  }
121
- maybePrintJson(log, !!json, keystore as unknown as Record<string, any>);
132
+ const blsKeyCandidates = getBlsKeyCandidates(summaries);
133
+ const outputData = blsKeyCandidates.length > 0 ? { ...keystore, blsKeyCandidates } : keystore;
134
+ maybePrintJson(log, !!json, outputData as unknown as Record<string, any>);
122
135
  }
@@ -0,0 +1,209 @@
1
+ import {
2
+ type ProofOfPossessionGasModel,
3
+ estimateBlsKeyProofOfPossessionGas,
4
+ minimumProofOfPossessionGas,
5
+ } from '@aztec-labs/ethereum/bn254';
6
+ import { deriveBlsPrivateKey } from '@aztec-labs/foundation/crypto/bls';
7
+ import type { Hex } from '@aztec-labs/foundation/string';
8
+
9
+ /** What a BLS key derivation version checks: a key fits if its estimated minimum stipend is at most `budget`. */
10
+ export type BlsKeyDerivationVersion = {
11
+ version: number;
12
+ gasModel: ProofOfPossessionGasModel;
13
+ budget: number;
14
+ };
15
+
16
+ /**
17
+ * BLS key derivation v6. The gas model is a frozen copy of `AMSTERDAM_PROOF_OF_POSSESSION_GAS_MODEL` from
18
+ * `@aztec-labs/ethereum/bn254`. The budget is the 300,000 gas `proofOfPossessionGasLimit` that the v6 upgrade
19
+ * payload sets on the GSE, minus a 10% margin.
20
+ *
21
+ * With per-attempt probabilities 0.189030554816 of reaching the square root and 0.094515277408 of succeeding, about
22
+ * 5.8243e-6 of keys (1 in 171,694) estimate over the budget, so they move to a later candidate under v6.
23
+ *
24
+ * Frozen once a CLI release ships it: changing any value changes which BLS key `--bls-key-derivation-v6` selects for
25
+ * the same mnemonic and indices, so operators could no longer regenerate their keys. Add a new version instead.
26
+ */
27
+ export const BLS_KEY_DERIVATION_V6: BlsKeyDerivationVersion = {
28
+ version: 6,
29
+ gasModel: {
30
+ fixed: 132_694,
31
+ perAttempt: 515,
32
+ perSqrtCall: 4_580,
33
+ memoryNumerator: 95_975,
34
+ memoryDenominator: 1_000_000,
35
+ },
36
+ budget: 270_000,
37
+ };
38
+
39
+ /** Version checked when no derivation flag is given. A future version gets its own flag next to `--bls-key-derivation-v6`. */
40
+ export const LATEST_BLS_KEY_DERIVATION = BLS_KEY_DERIVATION_V6;
41
+
42
+ /** Number of candidates tried per validator before key selection gives up. */
43
+ export const MAX_BLS_KEY_CANDIDATES = 256;
44
+
45
+ /**
46
+ * How a validator's BLS key is chosen among its candidates. The budget is `maxGas` when set (a stricter, user-chosen
47
+ * value), or the version's budget otherwise.
48
+ * - `check`: candidate 0, failing if it estimates over the budget (no `--bls-key-derivation-v6`).
49
+ * - `retry`: the first candidate within the budget (`--bls-key-derivation-v6`).
50
+ * - `none`: candidate 0, unchecked (`--skip-bls-key-gas-check`).
51
+ */
52
+ export type BlsKeyDerivationPolicy =
53
+ | { mode: 'check'; version: BlsKeyDerivationVersion; maxGas?: number }
54
+ | { mode: 'retry'; version: BlsKeyDerivationVersion; maxGas?: number }
55
+ | { mode: 'none' };
56
+
57
+ /** CLI options that choose the BLS key derivation policy. */
58
+ export type BlsKeyDerivationOptions = {
59
+ blsKeyDerivationV6?: boolean;
60
+ blsKeyDerivationMaxGas?: number;
61
+ skipBlsKeyGasCheck?: boolean;
62
+ };
63
+
64
+ /** A BLS key chosen for a validator, with the derivation path and candidate that produced it. */
65
+ export type SelectedBlsKey = {
66
+ privateKey: Hex<32>;
67
+ /** Full derivation path, including the candidate component when the candidate is not 0. */
68
+ path: string;
69
+ candidate: number;
70
+ /** The `--bls-key-derivation-max-gas` value, when it took part in selecting a candidate other than 0. */
71
+ maxGas?: number;
72
+ };
73
+
74
+ /**
75
+ * How the CLI handled a mnemonic it generated because none was passed: `shown` when it printed the mnemonic before key
76
+ * derivation, so the operator can pass it back, and `hidden` otherwise.
77
+ */
78
+ export type GeneratedMnemonic = 'shown' | 'hidden';
79
+
80
+ const GENERATED_MNEMONIC_HINTS: Record<GeneratedMnemonic, string> = {
81
+ shown:
82
+ ' This run generated its mnemonic, and re-running without --mnemonic generates a new one: to keep this ' +
83
+ 'mnemonic, pass it back with --mnemonic.',
84
+ hidden: ' This run generated a mnemonic without showing it, so re-running derives new keys from a new mnemonic.',
85
+ };
86
+
87
+ /** Thrown when candidate 0 estimates over the budget and no flag says what to do about it. */
88
+ export class BlsKeyOverGasBudgetError extends Error {
89
+ constructor(
90
+ public readonly path: string,
91
+ public readonly estimatedGas: number,
92
+ public readonly version: BlsKeyDerivationVersion,
93
+ public readonly maxGas: number | undefined,
94
+ generatedMnemonic: GeneratedMnemonic | undefined,
95
+ ) {
96
+ const budget =
97
+ maxGas === undefined
98
+ ? `the ${version.budget} budget of BLS key derivation v${version.version}, so it could exceed the L1 gas cap`
99
+ : `the ${maxGas} budget set by --bls-key-derivation-max-gas`;
100
+ const remedies =
101
+ maxGas === undefined
102
+ ? `Re-run with --bls-key-derivation-v${version.version} to select a cheaper key for this validator, or with ` +
103
+ `--skip-bls-key-gas-check to keep this key regardless (for example, a key already registered).`
104
+ : `Re-run with --bls-key-derivation-v${version.version} to select a cheaper key for this validator, or with a ` +
105
+ `higher --bls-key-derivation-max-gas.`;
106
+ super(
107
+ `The proof-of-possession check of the BLS key derived at ${path} is estimated at ${estimatedGas} gas, above ` +
108
+ `${budget}. ${remedies}` +
109
+ (generatedMnemonic ? GENERATED_MNEMONIC_HINTS[generatedMnemonic] : ''),
110
+ );
111
+ this.name = 'BlsKeyOverGasBudgetError';
112
+ }
113
+ }
114
+
115
+ /** Validates the BLS key derivation flags and turns them into a policy. */
116
+ export function resolveBlsKeyDerivationPolicy(options: BlsKeyDerivationOptions): BlsKeyDerivationPolicy {
117
+ const { blsKeyDerivationV6, blsKeyDerivationMaxGas: maxGas, skipBlsKeyGasCheck } = options;
118
+ const version = LATEST_BLS_KEY_DERIVATION;
119
+ if (skipBlsKeyGasCheck) {
120
+ if (blsKeyDerivationV6) {
121
+ throw new Error('--bls-key-derivation-v6 and --skip-bls-key-gas-check cannot be used together');
122
+ }
123
+ if (maxGas !== undefined) {
124
+ throw new Error('--bls-key-derivation-max-gas and --skip-bls-key-gas-check cannot be used together');
125
+ }
126
+ return { mode: 'none' };
127
+ }
128
+
129
+ if (maxGas !== undefined) {
130
+ if (!Number.isSafeInteger(maxGas)) {
131
+ throw new Error(`--bls-key-derivation-max-gas must be an integer, got ${maxGas}`);
132
+ }
133
+ if (maxGas > version.budget) {
134
+ throw new Error(
135
+ `--bls-key-derivation-max-gas can only lower the budget: ${maxGas} is above the ${version.budget} gas budget ` +
136
+ `of BLS key derivation v${version.version}.`,
137
+ );
138
+ }
139
+ const minimum = minimumProofOfPossessionGas(version.gasModel);
140
+ if (maxGas < minimum) {
141
+ throw new Error(
142
+ `--bls-key-derivation-max-gas ${maxGas} is below ${minimum}, the lowest proof-of-possession gas estimate any ` +
143
+ `key can have, so no key could fit.`,
144
+ );
145
+ }
146
+ }
147
+
148
+ return { mode: blsKeyDerivationV6 ? 'retry' : 'check', version, ...(maxGas !== undefined ? { maxGas } : {}) };
149
+ }
150
+
151
+ /** Derivation path of a candidate: candidate 0 is the per-validator path itself, others append `/<candidate>`. */
152
+ export function blsCandidatePath(perValidatorPath: string, candidate: number): string {
153
+ return candidate === 0 ? perValidatorPath : `${perValidatorPath}/${candidate}`;
154
+ }
155
+
156
+ /** Inputs to {@link selectBlsKey}. */
157
+ export type SelectBlsKeyInput = {
158
+ mnemonic?: string;
159
+ ikm?: string;
160
+ perValidatorPath: string;
161
+ /** Set when the CLI generated `mnemonic` in this run, to tell the operator how to keep or replace it on error. */
162
+ generatedMnemonic?: GeneratedMnemonic;
163
+ maxCandidates?: number;
164
+ };
165
+
166
+ /**
167
+ * Derives a validator's BLS key from `perValidatorPath` under the given policy. Only the mnemonic or IKM, the path
168
+ * and the policy decide the result, so re-running with the same inputs regenerates the same key.
169
+ */
170
+ export function selectBlsKey(policy: BlsKeyDerivationPolicy, input: SelectBlsKeyInput): SelectedBlsKey {
171
+ const { mnemonic, ikm, perValidatorPath, generatedMnemonic, maxCandidates = MAX_BLS_KEY_CANDIDATES } = input;
172
+ const candidate0 = { privateKey: deriveBlsPrivateKey(mnemonic, ikm, perValidatorPath), path: perValidatorPath };
173
+ if (policy.mode === 'none') {
174
+ return { ...candidate0, candidate: 0 };
175
+ }
176
+
177
+ const { gasModel } = policy.version;
178
+ const budget = policy.maxGas ?? policy.version.budget;
179
+ if (policy.mode === 'check') {
180
+ const estimatedGas = estimateBlsKeyProofOfPossessionGas(gasModel, candidate0.privateKey);
181
+ if (estimatedGas > budget) {
182
+ throw new BlsKeyOverGasBudgetError(
183
+ perValidatorPath,
184
+ estimatedGas,
185
+ policy.version,
186
+ policy.maxGas,
187
+ // An IKM takes precedence over the mnemonic, so the mnemonic does not decide the BLS key.
188
+ ikm ? undefined : generatedMnemonic,
189
+ );
190
+ }
191
+ return { ...candidate0, candidate: 0 };
192
+ }
193
+
194
+ for (let candidate = 0; candidate < maxCandidates; candidate++) {
195
+ const path = blsCandidatePath(perValidatorPath, candidate);
196
+ const privateKey = candidate === 0 ? candidate0.privateKey : deriveBlsPrivateKey(mnemonic, ikm, path);
197
+ if (estimateBlsKeyProofOfPossessionGas(gasModel, privateKey) <= budget) {
198
+ return {
199
+ privateKey,
200
+ path,
201
+ candidate,
202
+ ...(candidate > 0 && policy.maxGas !== undefined ? { maxGas: budget } : {}),
203
+ };
204
+ }
205
+ }
206
+ throw new Error(
207
+ `No BLS key within the ${budget} gas budget found for ` + `${perValidatorPath} after ${maxCandidates} candidates.`,
208
+ );
209
+ }
@@ -1,7 +1,7 @@
1
- import { deriveBlsPrivateKey } from '@aztec-labs/foundation/crypto/bls';
2
1
  import type { LogFn } from '@aztec-labs/foundation/log';
3
2
  import { writeFile } from 'fs/promises';
4
3
 
4
+ import { type BlsKeyDerivationOptions, resolveBlsKeyDerivationPolicy, selectBlsKey } from './bls_key_derivation.js';
5
5
  import { computeBlsPublicKeyCompressed, withValidatorIndex } from './shared.js';
6
6
  import { defaultBlsPath } from './utils.js';
7
7
 
@@ -13,18 +13,39 @@ export type GenerateBlsKeypairOptions = {
13
13
  compressed?: boolean;
14
14
  json?: boolean;
15
15
  out?: string;
16
- };
16
+ } & BlsKeyDerivationOptions;
17
17
 
18
18
  export async function generateBlsKeypair(options: GenerateBlsKeypairOptions, log: LogFn) {
19
19
  const { mnemonic, ikm, blsPath, compressed = true, json, out } = options;
20
- const path = withValidatorIndex(blsPath ?? defaultBlsPath, 0);
21
- const priv = deriveBlsPrivateKey(mnemonic, ikm, path);
20
+ const blsKeyDerivation = resolveBlsKeyDerivationPolicy(options);
21
+ const {
22
+ path,
23
+ privateKey: priv,
24
+ candidate,
25
+ maxGas,
26
+ } = selectBlsKey(blsKeyDerivation, {
27
+ mnemonic,
28
+ ikm,
29
+ perValidatorPath: withValidatorIndex(blsPath ?? defaultBlsPath, 0),
30
+ });
22
31
  const pub = await computeBlsPublicKeyCompressed(priv);
23
- const result = { path, privateKey: priv, publicKey: pub, format: compressed ? 'compressed' : 'uncompressed' };
32
+ const result = {
33
+ path,
34
+ ...(candidate > 0 ? { candidate } : {}),
35
+ ...(maxGas !== undefined ? { maxGas } : {}),
36
+ privateKey: priv,
37
+ publicKey: pub,
38
+ format: compressed ? 'compressed' : 'uncompressed',
39
+ };
24
40
  if (out) {
25
41
  await writeFile(out, JSON.stringify(result, null, 2), { encoding: 'utf-8' });
26
42
  if (!json) {
27
43
  log(`Wrote BLS keypair to ${out}`);
44
+ if (candidate > 0) {
45
+ log(
46
+ `BLS key derived from candidate ${candidate} at ${path}${maxGas !== undefined ? ` (max gas ${maxGas})` : ''}`,
47
+ );
48
+ }
28
49
  }
29
50
  }
30
51
  if (json || !out) {
@@ -1,9 +1,32 @@
1
+ import { minimumProofOfPossessionGas } from '@aztec-labs/ethereum/bn254';
1
2
  import type { LogFn } from '@aztec-labs/foundation/log';
2
3
  import { Command, Option } from 'commander';
3
4
 
4
5
  import { parseAztecAddress, parseEthereumAddress, parseHex, parseOptionalInteger } from '../../utils/commands.js';
6
+ import { BLS_KEY_DERIVATION_V6 } from './bls_key_derivation.js';
5
7
  import { defaultBlsPath } from './utils.js';
6
8
 
9
+ function blsKeyDerivationV6Option() {
10
+ return new Option(
11
+ '--bls-key-derivation-v6',
12
+ `Replace any BLS key whose estimated proof-of-possession gas exceeds the budget with the next deterministic candidate from the same mnemonic (BLS key derivation v6, budget ${BLS_KEY_DERIVATION_V6.budget} gas). ETH keys never change. Pass it again to regenerate the same keys.`,
13
+ );
14
+ }
15
+
16
+ function blsKeyDerivationMaxGasOption() {
17
+ return new Option(
18
+ '--bls-key-derivation-max-gas <gas>',
19
+ `Stricter budget for the estimated proof-of-possession verification gas of each BLS key. The L1 limit (GSE proofOfPossessionGasLimit) is 300,000 gas from the v6 upgrade, and governance may change it; the default budget is ${BLS_KEY_DERIVATION_V6.budget}, that cap minus a 10% margin. Must be between ${minimumProofOfPossessionGas(BLS_KEY_DERIVATION_V6.gasModel)} and ${BLS_KEY_DERIVATION_V6.budget}. Without --bls-key-derivation-v6, an over-budget key is an error. Pass the same value again to regenerate the same keys.`,
20
+ ).argParser(value => parseOptionalInteger(value));
21
+ }
22
+
23
+ function skipBlsKeyGasCheckOption() {
24
+ return new Option(
25
+ '--skip-bls-key-gas-check',
26
+ 'Keep the first derived BLS key even if its proof-of-possession check could exceed the L1 gas cap (for example, a key already registered).',
27
+ );
28
+ }
29
+
7
30
  export function injectCommands(program: Command, log: LogFn) {
8
31
  const group = program
9
32
  .command('validator-keys')
@@ -39,6 +62,9 @@ export function injectCommands(program: Command, log: LogFn) {
39
62
  .option('--remote-signer <url>', 'Default remote signer URL for accounts in this file')
40
63
  .option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
41
64
  .option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
65
+ .addOption(blsKeyDerivationV6Option())
66
+ .addOption(blsKeyDerivationMaxGasOption())
67
+ .addOption(skipBlsKeyGasCheckOption())
42
68
  .addOption(
43
69
  new Option('--password <str>', 'Shared password for writing ETH JSON V3 and BLS EIP-2335 keystore files').env(
44
70
  'AZTEC_KEYSTORE_PASSWORD',
@@ -103,6 +129,9 @@ export function injectCommands(program: Command, log: LogFn) {
103
129
  .option('--remote-signer <url>', 'Default remote signer URL for accounts in this file')
104
130
  .option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
105
131
  .option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
132
+ .addOption(blsKeyDerivationV6Option())
133
+ .addOption(blsKeyDerivationMaxGasOption())
134
+ .addOption(skipBlsKeyGasCheckOption())
106
135
  .option('--empty', 'Generate an empty skeleton without keys')
107
136
  .option(
108
137
  '--password <str>',
@@ -168,6 +197,9 @@ export function injectCommands(program: Command, log: LogFn) {
168
197
  .option('--mnemonic <mnemonic>', 'Mnemonic for BLS derivation')
169
198
  .option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
170
199
  .option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
200
+ .addOption(blsKeyDerivationV6Option())
201
+ .addOption(blsKeyDerivationMaxGasOption())
202
+ .addOption(skipBlsKeyGasCheckOption())
171
203
  .option('--g2', 'Derive on G2 subgroup')
172
204
  .option('--compressed', 'Output compressed public key')
173
205
  .option('--json', 'Print JSON output to stdout')