@aztec-labs/cli 6.0.0-rc.2 → 6.0.0-rc.4
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.
- package/dest/cmds/infrastructure/index.d.ts +1 -1
- package/dest/cmds/infrastructure/index.d.ts.map +1 -1
- package/dest/cmds/infrastructure/index.js +3 -2
- package/dest/cmds/infrastructure/sequencers.d.ts +4 -1
- package/dest/cmds/infrastructure/sequencers.d.ts.map +1 -1
- package/dest/cmds/infrastructure/sequencers.js +10 -3
- package/dest/cmds/l1/deploy_l1_contracts_cmd.d.ts +1 -1
- package/dest/cmds/l1/deploy_l1_contracts_cmd.d.ts.map +1 -1
- package/dest/cmds/l1/deploy_l1_contracts_cmd.js +1 -0
- package/dest/cmds/l1/index.d.ts +1 -1
- package/dest/cmds/l1/index.d.ts.map +1 -1
- package/dest/cmds/l1/index.js +2 -1
- package/dest/cmds/l1/l1_rpc_test_utils.d.ts +10 -2
- package/dest/cmds/l1/l1_rpc_test_utils.d.ts.map +1 -1
- package/dest/cmds/l1/l1_rpc_test_utils.js +35 -1
- package/dest/cmds/l1/prover_stats.d.ts +1 -1
- package/dest/cmds/l1/prover_stats.d.ts.map +1 -1
- package/dest/cmds/l1/prover_stats.js +3 -4
- package/dest/cmds/l1/update_l1_validators.d.ts +3 -2
- package/dest/cmds/l1/update_l1_validators.d.ts.map +1 -1
- package/dest/cmds/l1/update_l1_validators.js +6 -4
- package/dest/cmds/misc/generate_p2p_private_key.d.ts +1 -1
- package/dest/cmds/misc/generate_p2p_private_key.d.ts.map +1 -1
- package/dest/cmds/misc/generate_p2p_private_key.js +1 -0
- package/dest/cmds/validator_keys/add.d.ts +1 -1
- package/dest/cmds/validator_keys/add.d.ts.map +1 -1
- package/dest/cmds/validator_keys/add.js +14 -4
- package/dest/cmds/validator_keys/bls_key_derivation.d.ts +89 -0
- package/dest/cmds/validator_keys/bls_key_derivation.d.ts.map +1 -0
- package/dest/cmds/validator_keys/bls_key_derivation.js +122 -0
- package/dest/cmds/validator_keys/generate_bls_keypair.d.ts +3 -2
- package/dest/cmds/validator_keys/generate_bls_keypair.d.ts.map +1 -1
- package/dest/cmds/validator_keys/generate_bls_keypair.js +16 -3
- package/dest/cmds/validator_keys/index.d.ts +1 -1
- package/dest/cmds/validator_keys/index.d.ts.map +1 -1
- package/dest/cmds/validator_keys/index.js +14 -3
- package/dest/cmds/validator_keys/new.d.ts +3 -2
- package/dest/cmds/validator_keys/new.d.ts.map +1 -1
- package/dest/cmds/validator_keys/new.js +47 -33
- package/dest/cmds/validator_keys/shared.d.ts +28 -3
- package/dest/cmds/validator_keys/shared.d.ts.map +1 -1
- package/dest/cmds/validator_keys/shared.js +48 -7
- package/dest/cmds/validator_keys/staker.d.ts +6 -1
- package/dest/cmds/validator_keys/staker.d.ts.map +1 -1
- package/dest/cmds/validator_keys/staker.js +32 -0
- package/dest/config/chain_l2_config.d.ts +1 -1
- package/dest/config/chain_l2_config.d.ts.map +1 -1
- package/dest/config/chain_l2_config.js +1 -1
- package/dest/config/enrich_env.d.ts +1 -1
- package/dest/config/enrich_env.d.ts.map +1 -1
- package/dest/config/enrich_env.js +1 -1
- package/dest/config/generated/networks.d.ts +2 -2
- package/dest/config/generated/networks.js +2 -2
- package/dest/config/get_l1_config.d.ts +1 -1
- package/dest/config/get_l1_config.d.ts.map +1 -1
- package/dest/config/get_l1_config.js +2 -1
- package/dest/config/network_config.d.ts +1 -1
- package/dest/config/network_config.d.ts.map +1 -1
- package/dest/config/network_config.js +4 -2
- package/dest/utils/aztec.d.ts +1 -1
- package/dest/utils/aztec.d.ts.map +1 -1
- package/dest/utils/aztec.js +1 -0
- package/dest/utils/commands.d.ts +1 -1
- package/dest/utils/commands.d.ts.map +1 -1
- package/dest/utils/commands.js +3 -1
- package/dest/utils/encoding.d.ts +1 -1
- package/dest/utils/encoding.d.ts.map +1 -1
- package/dest/utils/encoding.js +1 -0
- package/package.json +25 -25
- package/src/cmds/infrastructure/index.ts +13 -1
- package/src/cmds/infrastructure/sequencers.ts +14 -4
- package/src/cmds/l1/deploy_l1_contracts_cmd.ts +1 -0
- package/src/cmds/l1/index.ts +6 -0
- package/src/cmds/l1/l1_rpc_test_utils.ts +25 -2
- package/src/cmds/l1/prover_stats.ts +3 -2
- package/src/cmds/l1/update_l1_validators.ts +10 -4
- package/src/cmds/misc/generate_p2p_private_key.ts +1 -0
- package/src/cmds/validator_keys/add.ts +15 -2
- package/src/cmds/validator_keys/bls_key_derivation.ts +209 -0
- package/src/cmds/validator_keys/generate_bls_keypair.ts +26 -5
- package/src/cmds/validator_keys/index.ts +32 -0
- package/src/cmds/validator_keys/new.ts +50 -32
- package/src/cmds/validator_keys/shared.ts +70 -8
- package/src/cmds/validator_keys/staker.ts +56 -0
- package/src/config/chain_l2_config.ts +1 -0
- package/src/config/enrich_env.ts +1 -0
- package/src/config/generated/networks.ts +2 -2
- package/src/config/get_l1_config.ts +2 -1
- package/src/config/network_config.ts +4 -2
- package/src/utils/aztec.ts +1 -0
- package/src/utils/commands.ts +3 -1
- 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
|
-
|
|
113
|
-
|
|
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';
|
package/src/cmds/l1/index.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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:
|
|
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(
|
|
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(),
|
|
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
|
|
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`);
|
|
@@ -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, {
|
|
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
|
-
|
|
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
|
|
21
|
-
const
|
|
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 = {
|
|
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')
|