Skip to content

Commit 58bf01e

Browse files
authored
feat: aztec valkeys CLI on top of bb.js BN254 ecc (#18018)
Merging #17968 & #17848
2 parents d48cf30 + b16a8e4 commit 58bf01e

File tree

25 files changed

+2503
-21
lines changed

25 files changed

+2503
-21
lines changed

yarn-project/aztec/src/bin/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { injectCommands as injectContractCommands } from '@aztec/cli/contracts';
77
import { injectCommands as injectInfrastructureCommands } from '@aztec/cli/infrastructure';
88
import { injectCommands as injectL1Commands } from '@aztec/cli/l1';
99
import { injectCommands as injectMiscCommands } from '@aztec/cli/misc';
10+
import { injectCommands as injectValidatorKeysCommands } from '@aztec/cli/validator_keys';
1011
import { getActiveNetworkName } from '@aztec/foundation/config';
1112
import { createConsoleLogger, createLogger } from '@aztec/foundation/log';
1213

@@ -51,6 +52,7 @@ async function main() {
5152
program = injectL1Commands(program, userLog, debugLogger);
5253
program = injectAztecNodeCommands(program, userLog, debugLogger);
5354
program = injectMiscCommands(program, userLog);
55+
program = injectValidatorKeysCommands(program, userLog);
5456

5557
await program.parseAsync(process.argv);
5658
}

yarn-project/cli/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"./aztec_node": "./dest/cmds/aztec_node/index.js",
1111
"./cli-utils": "./dest/utils/index.js",
1212
"./misc": "./dest/cmds/misc/index.js",
13+
"./validator_keys": "./dest/cmds/validator_keys/index.js",
1314
"./setup-contracts": "./dest/cmds/misc/setup_contracts.js",
1415
"./utils": "./dest/utils/index.js",
1516
"./inspect": "./dest/utils/inspect.js",
@@ -78,14 +79,17 @@
7879
"@aztec/ethereum": "workspace:^",
7980
"@aztec/foundation": "workspace:^",
8081
"@aztec/l1-artifacts": "workspace:^",
82+
"@aztec/node-keystore": "workspace:^",
8183
"@aztec/node-lib": "workspace:^",
8284
"@aztec/p2p": "workspace:^",
8385
"@aztec/protocol-contracts": "workspace:^",
8486
"@aztec/stdlib": "workspace:^",
8587
"@aztec/test-wallet": "workspace:^",
8688
"@aztec/world-state": "workspace:^",
89+
"@ethersproject/wallet": "^5.8.0",
8790
"@iarna/toml": "^2.2.5",
8891
"@libp2p/peer-id-factory": "^3.0.4",
92+
"@scure/bip39": "^2.0.1",
8993
"commander": "^12.1.0",
9094
"lodash.chunk": "^4.2.0",
9195
"lodash.groupby": "^4.6.0",
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { EthAddress } from '@aztec/foundation/eth-address';
2+
import type { LogFn } from '@aztec/foundation/log';
3+
import { loadKeystoreFile } from '@aztec/node-keystore/loader';
4+
import type { KeyStore } from '@aztec/node-keystore/types';
5+
6+
import { wordlist } from '@scure/bip39/wordlists/english.js';
7+
import { dirname, isAbsolute, join } from 'path';
8+
import { generateMnemonic } from 'viem/accounts';
9+
10+
import type { NewValidatorKeystoreOptions } from './new.js';
11+
import {
12+
buildValidatorEntries,
13+
logValidatorSummaries,
14+
maybePrintJson,
15+
writeBlsBn254ToFile,
16+
writeEthJsonV3ToFile,
17+
writeKeystoreFile,
18+
} from './shared.js';
19+
20+
export type AddValidatorKeysOptions = NewValidatorKeystoreOptions;
21+
22+
export async function addValidatorKeys(existing: string, options: AddValidatorKeysOptions, log: LogFn) {
23+
const {
24+
dataDir,
25+
file,
26+
count,
27+
publisherCount = 0,
28+
mnemonic,
29+
accountIndex = 0,
30+
addressIndex,
31+
ikm,
32+
blsPath,
33+
blsOnly,
34+
json,
35+
feeRecipient: feeRecipientOpt,
36+
coinbase: coinbaseOpt,
37+
fundingAccount: fundingAccountOpt,
38+
remoteSigner: remoteSignerOpt,
39+
password,
40+
outDir,
41+
} = options;
42+
43+
const validatorCount = typeof count === 'number' && Number.isFinite(count) && count > 0 ? Math.floor(count) : 1;
44+
const baseAddressIndex = addressIndex ?? 0;
45+
46+
const keystore: KeyStore = loadKeystoreFile(existing);
47+
48+
if (!keystore.validators || !Array.isArray(keystore.validators)) {
49+
throw new Error('Invalid keystore: missing validators array');
50+
}
51+
52+
const first = keystore.validators[0] ?? {};
53+
const feeRecipient = feeRecipientOpt ?? first.feeRecipient;
54+
if (!feeRecipient) {
55+
throw new Error('feeRecipient is required (either present in existing file or via --fee-recipient)');
56+
}
57+
const coinbase = (coinbaseOpt as EthAddress | undefined) ?? (first.coinbase as EthAddress | undefined);
58+
const fundingAccount =
59+
(fundingAccountOpt as EthAddress | undefined) ?? (first.fundingAccount as EthAddress | undefined);
60+
const derivedRemoteSigner = (first.attester as any)?.remoteSignerUrl || (first.attester as any)?.eth?.remoteSignerUrl;
61+
const remoteSigner = remoteSignerOpt ?? derivedRemoteSigner;
62+
63+
// Ensure we always have a mnemonic for key derivation if none was provided
64+
const mnemonicToUse = mnemonic ?? generateMnemonic(wordlist);
65+
66+
// If user explicitly provided --address-index, use it as-is. Otherwise, append after existing validators.
67+
const effectiveBaseAddressIndex =
68+
addressIndex === undefined ? baseAddressIndex + keystore.validators.length : baseAddressIndex;
69+
70+
const { validators, summaries } = await buildValidatorEntries({
71+
validatorCount,
72+
publisherCount,
73+
accountIndex,
74+
baseAddressIndex: effectiveBaseAddressIndex,
75+
mnemonic: mnemonicToUse,
76+
ikm,
77+
blsPath,
78+
blsOnly,
79+
feeRecipient,
80+
coinbase,
81+
remoteSigner,
82+
fundingAccount,
83+
});
84+
85+
keystore.validators.push(...validators);
86+
87+
// If password provided, write ETH JSON V3 and BLS BN254 keystores and replace plaintext
88+
if (password !== undefined) {
89+
const targetDir =
90+
outDir && outDir.length > 0 ? outDir : dataDir && dataDir.length > 0 ? dataDir : dirname(existing);
91+
await writeEthJsonV3ToFile(keystore.validators, { outDir: targetDir, password });
92+
await writeBlsBn254ToFile(keystore.validators, { outDir: targetDir, password });
93+
}
94+
95+
let outputPath = existing;
96+
if (file && file.length > 0) {
97+
if (isAbsolute(file)) {
98+
outputPath = file;
99+
} else if (dataDir && dataDir.length > 0) {
100+
outputPath = join(dataDir, file);
101+
} else {
102+
outputPath = join(dirname(existing), file);
103+
}
104+
}
105+
106+
await writeKeystoreFile(outputPath, keystore);
107+
108+
if (!json) {
109+
log(`Updated keystore ${outputPath} with ${validators.length} new validator(s)`);
110+
logValidatorSummaries(log, summaries);
111+
}
112+
maybePrintJson(log, !!json, keystore as unknown as Record<string, any>);
113+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { deriveBlsPrivateKey } from '@aztec/foundation/crypto';
2+
import type { LogFn } from '@aztec/foundation/log';
3+
4+
import { writeFile } from 'fs/promises';
5+
6+
import { computeBlsPublicKeyCompressed, withValidatorIndex } from './shared.js';
7+
8+
export type GenerateBlsKeypairOptions = {
9+
mnemonic?: string;
10+
ikm?: string;
11+
blsPath?: string;
12+
g2?: boolean;
13+
compressed?: boolean;
14+
json?: boolean;
15+
out?: string;
16+
};
17+
18+
export async function generateBlsKeypair(options: GenerateBlsKeypairOptions, log: LogFn) {
19+
const { mnemonic, ikm, blsPath, compressed = true, json, out } = options;
20+
const path = withValidatorIndex(blsPath ?? 'm/12381/3600/0/0/0', 0);
21+
const priv = deriveBlsPrivateKey(mnemonic, ikm, path);
22+
const pub = await computeBlsPublicKeyCompressed(priv);
23+
const result = { path, privateKey: priv, publicKey: pub, format: compressed ? 'compressed' : 'uncompressed' };
24+
if (out) {
25+
await writeFile(out, JSON.stringify(result, null, 2), { encoding: 'utf-8' });
26+
if (!json) {
27+
log(`Wrote BLS keypair to ${out}`);
28+
}
29+
}
30+
if (json || !out) {
31+
log(JSON.stringify(result, null, 2));
32+
}
33+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { LogFn } from '@aztec/foundation/log';
2+
3+
import { Command } from 'commander';
4+
5+
import { parseAztecAddress, parseEthereumAddress, parseHex, parseOptionalInteger } from '../../utils/commands.js';
6+
7+
export function injectCommands(program: Command, log: LogFn) {
8+
const group = program
9+
.command('validator-keys')
10+
.aliases(['valKeys'])
11+
.description('Manage validator keystores for node operators');
12+
13+
group
14+
.command('new')
15+
.summary('Generate a new validator keystore JSON')
16+
.description('Generates a new validator keystore with ETH secp256k1 accounts and optional BLS accounts')
17+
.option('--data-dir <path>', 'Directory to store keystore(s). Defaults to ~/.aztec/keystore')
18+
.option('--file <name>', 'Keystore file name. Defaults to key1.json (or keyN.json if key1.json exists)')
19+
.option('--count <N>', 'Number of validators to generate', parseOptionalInteger)
20+
.option('--publisher-count <N>', 'Number of publisher accounts per validator (default 1)', value =>
21+
parseOptionalInteger(value, 0),
22+
)
23+
.option('--mnemonic <mnemonic>', 'Mnemonic for ETH/BLS derivation')
24+
.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)
28+
.option('--funding-account <address>', 'ETH account to fund publishers', parseEthereumAddress)
29+
.option('--remote-signer <url>', 'Default remote signer URL for accounts in this file')
30+
.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)')
32+
.option('--bls-only', 'Generate only BLS keys')
33+
.option(
34+
'--password <str>',
35+
'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335). Empty string allowed',
36+
)
37+
.option('--out-dir <dir>', 'Output directory for generated keystore file(s)')
38+
.option('--json', 'Echo resulting JSON to stdout')
39+
.requiredOption('--fee-recipient <address>', 'Aztec address that will receive fees', parseAztecAddress)
40+
.action(async options => {
41+
const { newValidatorKeystore } = await import('./new.js');
42+
await newValidatorKeystore(options, log);
43+
});
44+
45+
group
46+
.command('add')
47+
.summary('Augment an existing validator keystore JSON')
48+
.description('Adds attester/publisher/BLS entries to an existing keystore using the same flags as new')
49+
.argument('<existing>', 'Path to existing keystore JSON')
50+
.option('--data-dir <path>', 'Directory where keystore(s) live')
51+
.option('--file <name>', 'Override output file name')
52+
.option('--count <N>', 'Number of validators to add', parseOptionalInteger)
53+
.option('--publisher-count <N>', 'Number of publisher accounts per validator (default 1)', value =>
54+
parseOptionalInteger(value, 0),
55+
)
56+
.option('--mnemonic <mnemonic>', 'Mnemonic for ETH/BLS derivation')
57+
.option('--passphrase <str>', 'Optional passphrase for mnemonic')
58+
.option('--account-index <N>', 'Base account index for ETH derivation', parseOptionalInteger)
59+
.option('--address-index <N>', 'Base address index for ETH derivation', parseOptionalInteger)
60+
.option('--coinbase <address>', 'Coinbase ETH address to use when proposing', parseEthereumAddress)
61+
.option('--funding-account <address>', 'ETH account to fund publishers', parseEthereumAddress)
62+
.option('--remote-signer <url>', 'Default remote signer URL for accounts in this file')
63+
.option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
64+
.option('--bls-path <path>', 'EIP-2334 path (default m/12381/3600/0/0/0)')
65+
.option('--bls-only', 'Generate only BLS keys')
66+
.option('--empty', 'Generate an empty skeleton without keys')
67+
.option(
68+
'--password <str>',
69+
'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335). Empty string allowed',
70+
)
71+
.option('--out-dir <dir>', 'Output directory for generated keystore file(s)')
72+
.option('--json', 'Echo resulting JSON to stdout')
73+
.requiredOption('--fee-recipient <address>', 'Aztec address that will receive fees', parseAztecAddress)
74+
.action(async (existing: string, options) => {
75+
const { addValidatorKeys } = await import('./add.js');
76+
await addValidatorKeys(existing, options, log);
77+
});
78+
79+
// top-level convenience: aztec generate-bls-keypair
80+
program
81+
.command('generate-bls-keypair')
82+
.description('Generate a BLS keypair with convenience flags')
83+
.option('--mnemonic <mnemonic>', 'Mnemonic for BLS derivation')
84+
.option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
85+
.option('--bls-path <path>', 'EIP-2334 path (default m/12381/3600/0/0/0)')
86+
.option('--g2', 'Derive on G2 subgroup')
87+
.option('--compressed', 'Output compressed public key')
88+
.option('--json', 'Print JSON output to stdout')
89+
.option('--out <file>', 'Write output to file')
90+
.action(async options => {
91+
const { generateBlsKeypair } = await import('./generate_bls_keypair.js');
92+
await generateBlsKeypair(options, log);
93+
});
94+
95+
return program;
96+
}

0 commit comments

Comments
 (0)