Skip to content

Commit 663bb9e

Browse files
authored
chore: Accumulated backports to v5-next (#24497)
BEGIN_COMMIT_OVERRIDE fix: improve valkeys password handling (#24459) feat: allow separate valkeys passwords. (#24460) feat: speed up valkeys generation (#24467) END_COMMIT_OVERRIDE
2 parents 888e322 + b7f8bbd commit 663bb9e

9 files changed

Lines changed: 709 additions & 118 deletions

File tree

docs/docs-operate/operators/keystore/creating-keystores.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,28 @@ This creates keystores at:
394394
- **Directory**: `~/.aztec/keystore/`
395395
- **Filename**: `key1.json`, `key2.json`, etc. (auto-increments)
396396

397+
### Encrypted Keystore Passwords
398+
399+
Use `--password`, `--password-file`, or `AZTEC_KEYSTORE_PASSWORD` to write encrypted ETH JSON V3 and BLS
400+
EIP-2335 keystore files with a shared password. To use different passwords, provide role-specific sources such as
401+
`--eth-password` and `--bls-password`.
402+
403+
```bash
404+
export AZTEC_KEYSTORE_PASSWORD='your-secure-password'
405+
aztec validator-keys new \
406+
--fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \
407+
--count 5 \
408+
--publishers 0x7988a4a779f058a0 \
409+
--encrypted-keystore-dir ~/.aztec/keystore/encrypted
410+
```
411+
412+
For each key type, role-specific sources take precedence over shared sources: `--eth-password`,
413+
`--eth-password-file`, and `AZTEC_ETH_KEYSTORE_PASSWORD` for ETH keystores; `--bls-password`,
414+
`--bls-password-file`, and `AZTEC_BLS_KEYSTORE_PASSWORD` for BLS keystores. Shared sources use `--password`,
415+
`--password-file`, then `AZTEC_KEYSTORE_PASSWORD`. Passwords cannot be empty. When the command generates a mnemonic
416+
while writing encrypted keystores, it does not print the mnemonic to stdout; provide `--mnemonic` if you need deterministic
417+
recovery.
418+
397419
## Verifying Your Keystore
398420

399421
Verify the keystore is valid JSON:

docs/docs-operate/operators/keystore/troubleshooting.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,13 @@ aztec validator-keys new [options]
370370
| `--gse-address <address>` | GSE contract address (required with --staker-output) | None |
371371
| `--l1-rpc-urls <urls>` | L1 RPC endpoints (required with --staker-output) | None |
372372
| `--legacy` | Use 2.1.4 BLS derivation path (only for regenerating old keys) | `false` |
373+
| `--password <str>` | Shared password for encrypted ETH and BLS keystore files | `AZTEC_KEYSTORE_PASSWORD` if set |
374+
| `--password-file <path>` | File containing the shared encrypted-keystore password | None |
375+
| `--eth-password <str>` | Password for encrypted ETH keystore files | `AZTEC_ETH_KEYSTORE_PASSWORD`, then shared password |
376+
| `--eth-password-file <path>` | File containing the ETH encrypted-keystore password | None |
377+
| `--bls-password <str>` | Password for encrypted BLS keystore files | `AZTEC_BLS_KEYSTORE_PASSWORD`, then shared password |
378+
| `--bls-password-file <path>` | File containing the BLS encrypted-keystore password | None |
379+
| `--encrypted-keystore-dir <path>` | Directory for encrypted ETH and BLS keystore files | Keystore output directory |
373380
| `--data-dir <path>` | Output directory | `~/.aztec/keystore` |
374381
| `--file <name>` | Keystore filename | `key1.json` |
375382

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Wallet } from '@ethersproject/wallet';
2+
import { parentPort, workerData } from 'worker_threads';
3+
4+
type EthJsonV3WorkerData = {
5+
privateKeyHex: string;
6+
password: string;
7+
};
8+
9+
type SerializedError = {
10+
message: string;
11+
name?: string;
12+
stack?: string;
13+
};
14+
15+
function serializeError(error: unknown): SerializedError {
16+
if (error instanceof Error) {
17+
return { message: error.message, name: error.name, stack: error.stack };
18+
}
19+
return { message: String(error) };
20+
}
21+
22+
void (async () => {
23+
try {
24+
const { privateKeyHex, password } = workerData as EthJsonV3WorkerData;
25+
const json = await new Wallet(privateKeyHex).encrypt(password);
26+
parentPort?.postMessage({ json });
27+
} catch (error) {
28+
parentPort?.postMessage({ error: serializeError(error) });
29+
}
30+
})();

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { LogFn } from '@aztec/foundation/log';
22

3-
import { Command } from 'commander';
3+
import { Command, Option } from 'commander';
44

55
import { parseAztecAddress, parseEthereumAddress, parseHex, parseOptionalInteger } from '../../utils/commands.js';
66
import { defaultBlsPath } from './utils.js';
@@ -38,10 +38,24 @@ export function injectCommands(program: Command, log: LogFn) {
3838
.option('--remote-signer <url>', 'Default remote signer URL for accounts in this file')
3939
.option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
4040
.option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
41-
.option(
42-
'--password <str>',
43-
'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335). Empty string allowed',
41+
.addOption(
42+
new Option('--password <str>', 'Shared password for writing ETH JSON V3 and BLS EIP-2335 keystore files').env(
43+
'AZTEC_KEYSTORE_PASSWORD',
44+
),
45+
)
46+
.option('--password-file <path>', 'File containing the shared password for writing keystore files')
47+
.addOption(
48+
new Option('--eth-password <str>', 'Password for writing ETH JSON V3 keystore files').env(
49+
'AZTEC_ETH_KEYSTORE_PASSWORD',
50+
),
51+
)
52+
.option('--eth-password-file <path>', 'File containing the password for writing ETH JSON V3 keystore files')
53+
.addOption(
54+
new Option('--bls-password <str>', 'Password for writing BLS EIP-2335 keystore files').env(
55+
'AZTEC_BLS_KEYSTORE_PASSWORD',
56+
),
4457
)
58+
.option('--bls-password-file <path>', 'File containing the password for writing BLS EIP-2335 keystore files')
4559
.option('--encrypted-keystore-dir <dir>', 'Output directory for encrypted keystore file(s)')
4660
.option('--json', 'Echo resulting JSON to stdout')
4761
.option('--staker-output', 'Generate a single staker output JSON file with an array of validator entries')

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

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { LogFn } from '@aztec/foundation/log';
66
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
77

88
import { wordlist } from '@scure/bip39/wordlists/english.js';
9-
import { writeFile } from 'fs/promises';
9+
import { readFile, writeFile } from 'fs/promises';
1010
import { basename, dirname, join } from 'path';
1111
import { createPublicClient, fallback, http } from 'viem';
1212
import { generateMnemonic, mnemonicToAccount } from 'viem/accounts';
@@ -42,6 +42,11 @@ export type NewValidatorKeystoreOptions = {
4242
ikm?: string;
4343
blsPath?: string;
4444
password?: string;
45+
passwordFile?: string;
46+
ethPassword?: string;
47+
ethPasswordFile?: string;
48+
blsPassword?: string;
49+
blsPasswordFile?: string;
4550
encryptedKeystoreDir?: string;
4651
json?: boolean;
4752
feeRecipient: AztecAddress;
@@ -53,6 +58,86 @@ export type NewValidatorKeystoreOptions = {
5358
l1ChainId?: number;
5459
};
5560

61+
type PasswordSourceOptions = Pick<
62+
NewValidatorKeystoreOptions,
63+
'password' | 'passwordFile' | 'ethPassword' | 'ethPasswordFile' | 'blsPassword' | 'blsPasswordFile'
64+
>;
65+
66+
type PasswordSource = {
67+
password?: string;
68+
passwordFile?: string;
69+
passwordOption: string;
70+
passwordFileOption: string;
71+
};
72+
73+
function validatePassword(password: string, source: string): string {
74+
if (password.length === 0) {
75+
throw new Error(`${source} cannot be empty`);
76+
}
77+
return password;
78+
}
79+
80+
function stripOneTrailingNewline(password: string): string {
81+
if (password.endsWith('\n')) {
82+
password = password.slice(0, -1);
83+
}
84+
if (password.endsWith('\r')) {
85+
password = password.slice(0, -1);
86+
}
87+
return password;
88+
}
89+
90+
async function resolvePasswordSource(source: PasswordSource): Promise<string | undefined> {
91+
if (source.password !== undefined && source.passwordFile !== undefined) {
92+
throw new Error(`${source.passwordOption} and ${source.passwordFileOption} cannot be used together`);
93+
}
94+
if (source.password !== undefined) {
95+
return validatePassword(source.password, source.passwordOption);
96+
}
97+
if (source.passwordFile !== undefined) {
98+
if (source.passwordFile.length === 0) {
99+
throw new Error(`${source.passwordFileOption} cannot be empty`);
100+
}
101+
const password = stripOneTrailingNewline(await readFile(source.passwordFile, 'utf-8'));
102+
return validatePassword(password, source.passwordFileOption);
103+
}
104+
105+
return undefined;
106+
}
107+
108+
async function resolvePasswords(options: PasswordSourceOptions) {
109+
const sharedPassword = await resolvePasswordSource({
110+
password: options.password,
111+
passwordFile: options.passwordFile,
112+
passwordOption: '--password',
113+
passwordFileOption: '--password-file',
114+
});
115+
const ethPassword =
116+
(await resolvePasswordSource({
117+
password: options.ethPassword,
118+
passwordFile: options.ethPasswordFile,
119+
passwordOption: '--eth-password',
120+
passwordFileOption: '--eth-password-file',
121+
})) ?? sharedPassword;
122+
const blsPassword =
123+
(await resolvePasswordSource({
124+
password: options.blsPassword,
125+
passwordFile: options.blsPasswordFile,
126+
passwordOption: '--bls-password',
127+
passwordFileOption: '--bls-password-file',
128+
})) ?? sharedPassword;
129+
130+
if (ethPassword === undefined && blsPassword === undefined) {
131+
return {};
132+
}
133+
if (ethPassword === undefined || blsPassword === undefined) {
134+
throw new Error(
135+
'Both ETH and BLS passwords are required when writing encrypted keystores. Provide --password to use one password for both, or provide both --eth-password and --bls-password.',
136+
);
137+
}
138+
return { ethPassword, blsPassword };
139+
}
140+
56141
export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, log: LogFn) {
57142
// validate bls-path inputs before proceeding with key generation
58143
validateBlsPathOptions(options);
@@ -78,17 +163,31 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
78163
blsPath,
79164
ikm,
80165
mnemonic: _mnemonic,
81-
password,
166+
password: passwordOption,
167+
passwordFile,
168+
ethPassword: ethPasswordOption,
169+
ethPasswordFile,
170+
blsPassword: blsPasswordOption,
171+
blsPasswordFile,
82172
encryptedKeystoreDir,
83173
stakerOutput,
84174
gseAddress,
85175
l1RpcUrls,
86176
l1ChainId,
87177
} = options;
88178

179+
const { ethPassword, blsPassword } = await resolvePasswords({
180+
password: passwordOption,
181+
passwordFile,
182+
ethPassword: ethPasswordOption,
183+
ethPasswordFile,
184+
blsPassword: blsPasswordOption,
185+
blsPasswordFile,
186+
});
187+
const shouldEncryptKeystores = ethPassword !== undefined && blsPassword !== undefined;
89188
const mnemonic = _mnemonic ?? generateMnemonic(wordlist);
90189

91-
if (!_mnemonic && !json) {
190+
if (!_mnemonic && !json && !shouldEncryptKeystores) {
92191
log('No mnemonic provided, generating new one...');
93192
log(`Using new mnemonic:`);
94193
log('');
@@ -115,11 +214,11 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
115214
});
116215

117216
// If password provided, write ETH JSON V3 and BLS BN254 keystores and replace plaintext
118-
if (password !== undefined) {
217+
if (shouldEncryptKeystores) {
119218
const encryptedKeystoreOutDir =
120219
encryptedKeystoreDir && encryptedKeystoreDir.length > 0 ? encryptedKeystoreDir : keystoreOutDir;
121-
await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
122-
await writeBlsBn254ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
220+
await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password: ethPassword });
221+
await writeBlsBn254ToFile(validators, { outDir: encryptedKeystoreOutDir, password: blsPassword });
123222
}
124223

125224
const keystore = {
@@ -145,7 +244,7 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
145244
// Process each validator
146245
for (let i = 0; i < validators.length; i++) {
147246
const validator = validators[i];
148-
const outputs = await processAttesterAccounts(validator.attester, gse, password);
247+
const outputs = await processAttesterAccounts(validator.attester, gse);
149248

150249
// Collect all staker outputs
151250
for (let j = 0; j < outputs.length; j++) {
@@ -160,7 +259,7 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
160259
}
161260
}
162261

163-
const outputData = !_mnemonic ? { ...keystore, generatedMnemonic: mnemonic } : keystore;
262+
const outputData = !_mnemonic && !shouldEncryptKeystores ? { ...keystore, generatedMnemonic: mnemonic } : keystore;
164263

165264
// Handle JSON output
166265
if (json) {

0 commit comments

Comments
 (0)