Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/docs-operate/operators/keystore/creating-keystores.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,24 @@ This creates keystores at:
- **Directory**: `~/.aztec/keystore/`
- **Filename**: `key1.json`, `key2.json`, etc. (auto-increments)

### Encrypted Keystore Passwords

Use `--password`, `--password-file`, or `AZTEC_KEYSTORE_PASSWORD` to write encrypted ETH JSON V3 and BLS
EIP-2335 keystore files:

```bash
export AZTEC_KEYSTORE_PASSWORD='your-secure-password'
aztec validator-keys new \
--fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \
--count 5 \
--publishers 0x7988a4a779f058a0 \
--encrypted-keystore-dir ~/.aztec/keystore/encrypted
```

Password precedence is `--password`, then `--password-file`, then `AZTEC_KEYSTORE_PASSWORD`. Passwords cannot be
empty. When the command generates a mnemonic while writing encrypted keystores, it does not print the mnemonic to stdout;
provide `--mnemonic` if you need deterministic recovery.

## Verifying Your Keystore

Verify the keystore is valid JSON:
Expand Down
3 changes: 3 additions & 0 deletions docs/docs-operate/operators/keystore/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@ aztec validator-keys new [options]
| `--gse-address <address>` | GSE contract address (required with --staker-output) | None |
| `--l1-rpc-urls <urls>` | L1 RPC endpoints (required with --staker-output) | None |
| `--legacy` | Use 2.1.4 BLS derivation path (only for regenerating old keys) | `false` |
| `--password <str>` | Shared password for encrypted ETH and BLS keystore files | `AZTEC_KEYSTORE_PASSWORD` if set |
| `--password-file <path>` | File containing the shared encrypted-keystore password | None |
| `--encrypted-keystore-dir <path>` | Directory for encrypted ETH and BLS keystore files | Keystore output directory |
| `--data-dir <path>` | Output directory | `~/.aztec/keystore` |
| `--file <name>` | Keystore filename | `key1.json` |

Expand Down
10 changes: 6 additions & 4 deletions yarn-project/cli/src/cmds/validator_keys/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { LogFn } from '@aztec/foundation/log';

import { Command } from 'commander';
import { Command, Option } from 'commander';

import { parseAztecAddress, parseEthereumAddress, parseHex, parseOptionalInteger } from '../../utils/commands.js';
import { defaultBlsPath } from './utils.js';
Expand Down Expand Up @@ -38,10 +38,12 @@ export function injectCommands(program: Command, log: LogFn) {
.option('--remote-signer <url>', 'Default remote signer URL for accounts in this file')
.option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
.option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
.option(
'--password <str>',
'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335). Empty string allowed',
.addOption(
new Option('--password <str>', 'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335)').env(
'AZTEC_KEYSTORE_PASSWORD',
),
)
.option('--password-file <path>', 'File containing the password for writing keystore files')
.option('--encrypted-keystore-dir <dir>', 'Output directory for encrypted keystore file(s)')
.option('--json', 'Echo resulting JSON to stdout')
.option('--staker-output', 'Generate a single staker output JSON file with an array of validator entries')
Expand Down
49 changes: 44 additions & 5 deletions yarn-project/cli/src/cmds/validator_keys/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { LogFn } from '@aztec/foundation/log';
import type { AztecAddress } from '@aztec/stdlib/aztec-address';

import { wordlist } from '@scure/bip39/wordlists/english.js';
import { writeFile } from 'fs/promises';
import { readFile, writeFile } from 'fs/promises';
import { basename, dirname, join } from 'path';
import { createPublicClient, fallback, http } from 'viem';
import { generateMnemonic, mnemonicToAccount } from 'viem/accounts';
Expand Down Expand Up @@ -42,6 +42,7 @@ export type NewValidatorKeystoreOptions = {
ikm?: string;
blsPath?: string;
password?: string;
passwordFile?: string;
encryptedKeystoreDir?: string;
json?: boolean;
feeRecipient: AztecAddress;
Expand All @@ -53,6 +54,41 @@ export type NewValidatorKeystoreOptions = {
l1ChainId?: number;
};

function validatePassword(password: string, source: string): string {
if (password.length === 0) {
throw new Error(`${source} cannot be empty`);
}
return password;
}

function stripOneTrailingNewline(password: string): string {
if (password.endsWith('\n')) {
password = password.slice(0, -1);
}
if (password.endsWith('\r')) {
password = password.slice(0, -1);
}
return password;
}

async function resolvePassword(options: Pick<NewValidatorKeystoreOptions, 'password' | 'passwordFile'>) {
if (options.password !== undefined && options.passwordFile !== undefined) {
throw new Error('--password and --password-file cannot be used together');
}
if (options.password !== undefined) {
return validatePassword(options.password, '--password');
}
if (options.passwordFile !== undefined) {
if (options.passwordFile.length === 0) {
throw new Error('--password-file cannot be empty');
}
const password = stripOneTrailingNewline(await readFile(options.passwordFile, 'utf-8'));
return validatePassword(password, '--password-file');
}

return undefined;
}

export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, log: LogFn) {
// validate bls-path inputs before proceeding with key generation
validateBlsPathOptions(options);
Expand All @@ -78,17 +114,20 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
blsPath,
ikm,
mnemonic: _mnemonic,
password,
password: passwordOption,
passwordFile,
encryptedKeystoreDir,
stakerOutput,
gseAddress,
l1RpcUrls,
l1ChainId,
} = options;

const password = await resolvePassword({ password: passwordOption, passwordFile });
const shouldEncryptKeystores = password !== undefined;
const mnemonic = _mnemonic ?? generateMnemonic(wordlist);

if (!_mnemonic && !json) {
if (!_mnemonic && !json && !shouldEncryptKeystores) {
log('No mnemonic provided, generating new one...');
log(`Using new mnemonic:`);
log('');
Expand All @@ -115,7 +154,7 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
});

// If password provided, write ETH JSON V3 and BLS BN254 keystores and replace plaintext
if (password !== undefined) {
if (shouldEncryptKeystores) {
const encryptedKeystoreOutDir =
encryptedKeystoreDir && encryptedKeystoreDir.length > 0 ? encryptedKeystoreDir : keystoreOutDir;
await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
Expand Down Expand Up @@ -160,7 +199,7 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
}
}

const outputData = !_mnemonic ? { ...keystore, generatedMnemonic: mnemonic } : keystore;
const outputData = !_mnemonic && !shouldEncryptKeystores ? { ...keystore, generatedMnemonic: mnemonic } : keystore;

// Handle JSON output
if (json) {
Expand Down
Loading
Loading