@aztec/node-keystore 4.0.0-nightly.20250907 → 4.0.0-nightly.20260107

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/src/loader.ts CHANGED
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * Handles loading and parsing keystore configuration files.
5
5
  */
6
+ import { EthAddress } from '@aztec/foundation/eth-address';
6
7
  import { createLogger } from '@aztec/foundation/log';
8
+ import type { Hex } from '@aztec/foundation/string';
7
9
 
8
10
  import { readFileSync, readdirSync, statSync } from 'fs';
9
11
  import { extname, join } from 'path';
12
+ import { privateKeyToAddress } from 'viem/accounts';
10
13
 
11
14
  import { keystoreSchema } from './schemas.js';
12
15
  import type { EthAccounts, KeyStore } from './types.js';
@@ -39,7 +42,7 @@ export function loadKeystoreFile(filePath: string): KeyStore {
39
42
  const content = readFileSync(filePath, 'utf-8');
40
43
 
41
44
  // Parse JSON and validate with Zod schema (following Aztec patterns)
42
- return keystoreSchema.parse(JSON.parse(content)) as KeyStore;
45
+ return keystoreSchema.parse(JSON.parse(content));
43
46
  } catch (error) {
44
47
  if (error instanceof SyntaxError) {
45
48
  throw new KeyStoreLoadError('Invalid JSON format', filePath, error);
@@ -205,12 +208,18 @@ export function mergeKeystores(keystores: KeyStore[]): KeyStore {
205
208
  // Track attester addresses to prevent duplicates
206
209
  const attesterAddresses = new Set<string>();
207
210
 
211
+ // Determine schema version: use v2 if any input is v2
212
+ const schemaVersion = keystores.some(ks => ks.schemaVersion === 2) ? 2 : 1;
213
+
208
214
  const merged: KeyStore = {
209
- schemaVersion: 1,
215
+ schemaVersion,
210
216
  validators: [],
211
217
  slasher: undefined,
212
218
  remoteSigner: undefined,
213
219
  prover: undefined,
220
+ publisher: undefined,
221
+ coinbase: undefined,
222
+ feeRecipient: undefined,
214
223
  };
215
224
 
216
225
  for (let i = 0; i < keystores.length; i++) {
@@ -220,8 +229,9 @@ export function mergeKeystores(keystores: KeyStore[]): KeyStore {
220
229
  if (keystore.validators) {
221
230
  for (const validator of keystore.validators) {
222
231
  // Check for duplicate attester addresses
223
- const attesterKeys = extractAttesterKeys(validator.attester);
224
- for (const key of attesterKeys) {
232
+ const attesterKeys = extractAttesterAddresses(validator.attester);
233
+ for (let key of attesterKeys) {
234
+ key = key.toLowerCase();
225
235
  if (attesterAddresses.has(key)) {
226
236
  throw new KeyStoreLoadError(
227
237
  `Duplicate attester address ${key} found across keystore files`,
@@ -230,8 +240,18 @@ export function mergeKeystores(keystores: KeyStore[]): KeyStore {
230
240
  }
231
241
  attesterAddresses.add(key);
232
242
  }
243
+
244
+ // When merging v1 validators into a v2+ result, preserve original fallback behavior
245
+ // by explicitly setting publisher/coinbase/feeRecipient if they're missing
246
+ if (keystore.schemaVersion !== schemaVersion) {
247
+ throw new KeyStoreLoadError(
248
+ `Cannot merge keystores with different schema versions: ${keystore.schemaVersion} and ${schemaVersion}`,
249
+ `keystores[${i}].schemaVersion`,
250
+ );
251
+ } else {
252
+ merged.validators!.push(validator);
253
+ }
233
254
  }
234
- merged.validators!.push(...keystore.validators);
235
255
  }
236
256
 
237
257
  // Merge slasher (accumulate all)
@@ -264,6 +284,45 @@ export function mergeKeystores(keystores: KeyStore[]): KeyStore {
264
284
  }
265
285
  merged.prover = keystore.prover;
266
286
  }
287
+
288
+ // Merge top-level publisher (accumulate all, unless conflicting MnemonicConfigs)
289
+ if (keystore.publisher) {
290
+ if (!merged.publisher) {
291
+ merged.publisher = keystore.publisher;
292
+ } else {
293
+ const isMnemonic = (accounts: EthAccounts): boolean =>
294
+ typeof accounts === 'object' && accounts !== null && 'mnemonic' in accounts;
295
+
296
+ // If either is a mnemonic, warn and use last one (can't merge mnemonics)
297
+ if (isMnemonic(merged.publisher) || isMnemonic(keystore.publisher)) {
298
+ logger.warn(
299
+ 'Multiple default publisher configurations found with mnemonic, using the last one (cannot merge mnemonics)',
300
+ );
301
+ merged.publisher = keystore.publisher;
302
+ } else {
303
+ // Both are non-mnemonic, accumulate them
304
+ const toArray = (accounts: EthAccounts): unknown[] => (Array.isArray(accounts) ? accounts : [accounts]);
305
+ const combined = [...toArray(merged.publisher), ...toArray(keystore.publisher)];
306
+ merged.publisher = combined as unknown as EthAccounts;
307
+ }
308
+ }
309
+ }
310
+
311
+ // Merge top-level coinbase (last one wins, but warn about conflicts)
312
+ if (keystore.coinbase) {
313
+ if (merged.coinbase) {
314
+ logger.warn('Multiple default coinbase addresses found, using the last one');
315
+ }
316
+ merged.coinbase = keystore.coinbase;
317
+ }
318
+
319
+ // Merge top-level feeRecipient (last one wins, but warn about conflicts)
320
+ if (keystore.feeRecipient) {
321
+ if (merged.feeRecipient) {
322
+ logger.warn('Multiple default feeRecipient addresses found, using the last one');
323
+ }
324
+ merged.feeRecipient = keystore.feeRecipient;
325
+ }
267
326
  }
268
327
 
269
328
  // Clean up empty arrays
@@ -284,18 +343,43 @@ export function mergeKeystores(keystores: KeyStore[]): KeyStore {
284
343
  * @param attester The attester configuration in any supported shape.
285
344
  * @returns Array of string keys used to detect duplicates.
286
345
  */
287
- function extractAttesterKeys(attester: unknown): string[] {
346
+ function extractAttesterAddresses(attester: unknown): string[] {
347
+ // String forms (private key or other) - return as-is for coarse uniqueness
288
348
  if (typeof attester === 'string') {
289
- return [attester];
349
+ if (attester.length === 66) {
350
+ return [privateKeyToAddress(attester as Hex<32>)];
351
+ } else {
352
+ return [attester];
353
+ }
290
354
  }
291
355
 
356
+ // Arrays of attester items
292
357
  if (Array.isArray(attester)) {
293
- return attester.map(a => (typeof a === 'string' ? a : JSON.stringify(a)));
358
+ const keys: string[] = [];
359
+ for (const item of attester) {
360
+ keys.push(...extractAttesterAddresses(item));
361
+ }
362
+ return keys;
294
363
  }
295
364
 
296
- if (attester && typeof attester === 'object' && 'address' in attester) {
297
- return [(attester as { address: string }).address];
365
+ if (attester && typeof attester === 'object') {
366
+ if (attester instanceof EthAddress) {
367
+ return [attester.toString()];
368
+ }
369
+
370
+ const obj = attester as Record<string, unknown>;
371
+
372
+ // New shape: { eth: EthAccount, bls?: BLSAccount }
373
+ if ('eth' in obj) {
374
+ return extractAttesterAddresses(obj.eth);
375
+ }
376
+
377
+ // Remote signer account object shape: { address, remoteSignerUrl?, ... }
378
+ if ('address' in obj) {
379
+ return [String((obj as any).address)];
380
+ }
298
381
  }
299
382
 
300
- return [JSON.stringify(attester)];
383
+ // mnemonic, encrypted file just disable early duplicates checking
384
+ return [];
301
385
  }
package/src/schemas.ts CHANGED
@@ -1,98 +1,175 @@
1
1
  /**
2
2
  * Zod schemas for keystore validation using Aztec's validation functions
3
3
  */
4
- import { EthAddress } from '@aztec/foundation/eth-address';
4
+ import { optional, schemas } from '@aztec/foundation/schemas';
5
5
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
6
6
 
7
7
  import { z } from 'zod';
8
8
 
9
+ import type { BLSPrivateKey, EthPrivateKey } from './types.js';
10
+
9
11
  // Use Aztec's validation functions but return string types to match our TypeScript interfaces
10
- const ethAddressSchema = z.string().refine(EthAddress.isAddress, 'Invalid Ethereum address');
11
- const ethPrivateKeySchema = z
12
+ export const ethPrivateKeySchema = z
13
+ .string()
14
+ .regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid private key (must be 32 bytes with 0x prefix)')
15
+ .transform(s => s as EthPrivateKey);
16
+ export const blsPrivateKeySchema = z
12
17
  .string()
13
- .regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid private key (must be 32 bytes with 0x prefix)');
14
- const aztecAddressSchema = z.string().refine(AztecAddress.isAddress, 'Invalid Aztec address');
18
+ .regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid BLS private key (must be 32 bytes with 0x prefix)')
19
+ .transform(s => s as BLSPrivateKey);
15
20
  const urlSchema = z.string().url('Invalid URL');
16
21
 
17
22
  // Remote signer config schema
18
23
  const remoteSignerConfigSchema = z.union([
19
24
  urlSchema,
20
- z.object({
21
- remoteSignerUrl: urlSchema,
22
- certPath: z.string().nullish(),
23
- certPass: z.string().nullish(),
24
- }),
25
+ z
26
+ .object({
27
+ remoteSignerUrl: urlSchema,
28
+ certPath: optional(z.string()),
29
+ certPass: optional(z.string()),
30
+ })
31
+ .strict(),
25
32
  ]);
26
33
 
27
34
  // Remote signer account schema
28
35
  const remoteSignerAccountSchema = z.union([
29
- ethAddressSchema,
30
- z.object({
31
- address: ethAddressSchema,
32
- remoteSignerUrl: urlSchema.nullish(),
33
- certPath: z.string().nullish(),
34
- certPass: z.string().nullish(),
35
- }),
36
+ schemas.EthAddress,
37
+ z
38
+ .object({
39
+ address: schemas.EthAddress,
40
+ remoteSignerUrl: urlSchema,
41
+ certPath: optional(z.string()),
42
+ certPass: optional(z.string()),
43
+ })
44
+ .strict(),
36
45
  ]);
37
46
 
38
- // JSON V3 keystore schema
39
- const jsonKeyFileV3Schema = z.object({
40
- path: z.string(),
41
- password: z.string().nullish(),
42
- });
47
+ // Encrypted keystore file schema (used for both JSON V3 ETH keys and EIP-2335 BLS keys)
48
+ const encryptedKeyFileSchema = z
49
+ .object({
50
+ path: z.string(),
51
+ password: optional(z.string()),
52
+ })
53
+ .strict();
43
54
 
44
55
  // Mnemonic config schema
45
- const mnemonicConfigSchema = z.object({
46
- mnemonic: z.string().min(1, 'Mnemonic cannot be empty'),
47
- addressIndex: z.number().int().min(0).default(0),
48
- accountIndex: z.number().int().min(0).default(0),
49
- addressCount: z.number().int().min(1).default(1),
50
- accountCount: z.number().int().min(1).default(1),
51
- });
56
+ const mnemonicConfigSchema = z
57
+ .object({
58
+ mnemonic: z.string().min(1, 'Mnemonic cannot be empty'),
59
+ addressIndex: z.number().int().min(0).default(0),
60
+ accountIndex: z.number().int().min(0).default(0),
61
+ addressCount: z.number().int().min(1).default(1),
62
+ accountCount: z.number().int().min(1).default(1),
63
+ })
64
+ .strict();
52
65
 
53
66
  // EthAccount schema
54
- const ethAccountSchema = z.union([
55
- ethPrivateKeySchema,
56
- remoteSignerAccountSchema,
57
- jsonKeyFileV3Schema,
58
- mnemonicConfigSchema,
59
- ]);
67
+ const ethAccountSchema = z.union([ethPrivateKeySchema, remoteSignerAccountSchema, encryptedKeyFileSchema]);
60
68
 
61
69
  // EthAccounts schema
62
- const ethAccountsSchema = z.union([ethAccountSchema, z.array(ethAccountSchema)]);
70
+ const ethAccountsSchema = z.union([ethAccountSchema, z.array(ethAccountSchema), mnemonicConfigSchema]);
71
+
72
+ // BLSAccount schema
73
+ const blsAccountSchema = z.union([blsPrivateKeySchema, encryptedKeyFileSchema]);
74
+
75
+ // AttesterAccount schema: either EthAccount or { eth: EthAccount, bls?: BLSAccount }
76
+ const attesterAccountSchema = z.union([
77
+ ethAccountSchema,
78
+ z
79
+ .object({
80
+ eth: ethAccountSchema,
81
+ bls: optional(blsAccountSchema),
82
+ })
83
+ .strict(),
84
+ ]);
85
+
86
+ // AttesterAccounts schema: AttesterAccount | AttesterAccount[] | MnemonicConfig
87
+ const attesterAccountsSchema = z.union([attesterAccountSchema, z.array(attesterAccountSchema), mnemonicConfigSchema]);
63
88
 
64
89
  // Prover keystore schema
65
90
  const proverKeyStoreSchema = z.union([
66
91
  ethAccountSchema,
67
- z.object({
68
- id: ethAddressSchema,
69
- publisher: ethAccountsSchema,
70
- }),
92
+ z
93
+ .object({
94
+ id: schemas.EthAddress,
95
+ publisher: ethAccountsSchema,
96
+ })
97
+ .strict(),
71
98
  ]);
72
99
 
73
- // Validator keystore schema
74
- const validatorKeyStoreSchema = z.object({
75
- attester: ethAccountsSchema,
76
- coinbase: ethAddressSchema.nullish(),
77
- publisher: ethAccountsSchema.nullish(),
78
- feeRecipient: aztecAddressSchema,
79
- remoteSigner: remoteSignerConfigSchema.nullish(),
80
- fundingAccount: ethAccountSchema.nullish(),
81
- });
82
-
83
- // Main keystore schema
84
- export const keystoreSchema = z
100
+ // Validator keystore schema for v1 (feeRecipient required)
101
+ const validatorKeyStoreSchemaV1 = z
102
+ .object({
103
+ attester: attesterAccountsSchema,
104
+ coinbase: optional(schemas.EthAddress),
105
+ publisher: optional(ethAccountsSchema),
106
+ feeRecipient: AztecAddress.schema,
107
+ remoteSigner: optional(remoteSignerConfigSchema),
108
+ fundingAccount: optional(ethAccountSchema),
109
+ })
110
+ .strict();
111
+
112
+ // Validator keystore schema for v2 (feeRecipient optional, can fall back to top-level)
113
+ const validatorKeyStoreSchemaV2 = z
114
+ .object({
115
+ attester: attesterAccountsSchema,
116
+ coinbase: optional(schemas.EthAddress),
117
+ publisher: optional(ethAccountsSchema),
118
+ feeRecipient: optional(AztecAddress.schema),
119
+ remoteSigner: optional(remoteSignerConfigSchema),
120
+ fundingAccount: optional(ethAccountSchema),
121
+ })
122
+ .strict();
123
+
124
+ // Schema v1 - original format
125
+ const keystoreSchemaV1 = z
85
126
  .object({
86
127
  schemaVersion: z.literal(1),
87
- validators: z.array(validatorKeyStoreSchema).nullish(),
88
- slasher: ethAccountsSchema.nullish(),
89
- remoteSigner: remoteSignerConfigSchema.nullish(),
90
- prover: proverKeyStoreSchema.nullish(),
91
- fundingAccount: ethAccountSchema.nullish(),
128
+ validators: optional(z.array(validatorKeyStoreSchemaV1)),
129
+ slasher: optional(ethAccountsSchema),
130
+ remoteSigner: optional(remoteSignerConfigSchema),
131
+ prover: optional(proverKeyStoreSchema),
132
+ fundingAccount: optional(ethAccountSchema),
92
133
  })
134
+ .strict()
93
135
  .refine(data => data.validators || data.prover, {
94
136
  message: 'Keystore must have at least validators or prover configuration',
95
137
  path: ['root'],
96
138
  });
97
139
 
98
- export type KeyStoreSchema = z.infer<typeof keystoreSchema>;
140
+ // Schema v2 - adds top-level publisher, coinbase, feeRecipient
141
+ const keystoreSchemaV2 = z
142
+ .object({
143
+ schemaVersion: z.literal(2),
144
+ validators: optional(z.array(validatorKeyStoreSchemaV2)),
145
+ slasher: optional(ethAccountsSchema),
146
+ remoteSigner: optional(remoteSignerConfigSchema),
147
+ prover: optional(proverKeyStoreSchema),
148
+ fundingAccount: optional(ethAccountSchema),
149
+ publisher: optional(ethAccountsSchema),
150
+ coinbase: optional(schemas.EthAddress),
151
+ feeRecipient: optional(AztecAddress.schema),
152
+ })
153
+ .strict()
154
+ .refine(data => data.validators || data.prover, {
155
+ message: 'Keystore must have at least validators or prover configuration',
156
+ path: ['root'],
157
+ })
158
+ .refine(
159
+ data => {
160
+ // If validators are present, ensure each validator has a feeRecipient or there's a top-level feeRecipient
161
+ if (data.validators) {
162
+ const hasTopLevelFeeRecipient = !!data.feeRecipient;
163
+ const allValidatorsHaveFeeRecipient = data.validators.every(v => v.feeRecipient);
164
+ return hasTopLevelFeeRecipient || allValidatorsHaveFeeRecipient;
165
+ }
166
+ return true;
167
+ },
168
+ {
169
+ message: 'Each validator must have a feeRecipient, or a top-level feeRecipient must be set for all validators',
170
+ path: ['feeRecipient'],
171
+ },
172
+ );
173
+
174
+ // Main keystore schema - accepts both v1 and v2
175
+ export const keystoreSchema = z.union([keystoreSchemaV1, keystoreSchemaV2]);
package/src/signer.ts CHANGED
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Common interface for different signing backends (local, remote, encrypted)
5
5
  */
6
- import type { EthSigner } from '@aztec/ethereum';
6
+ import type { EthSigner } from '@aztec/ethereum/eth-signer';
7
7
  import { Buffer32 } from '@aztec/foundation/buffer';
8
- import { Secp256k1Signer, randomBytes, toRecoveryBit } from '@aztec/foundation/crypto';
8
+ import { randomBytes } from '@aztec/foundation/crypto/random';
9
+ import { Secp256k1Signer, toRecoveryBit } from '@aztec/foundation/crypto/secp256k1-signer';
9
10
  import type { EthAddress } from '@aztec/foundation/eth-address';
10
11
  import { Signature, type ViemTransactionSignature } from '@aztec/foundation/eth-signature';
11
12
  import { jsonStringify } from '@aztec/foundation/json-rpc';
12
- import { withHexPrefix } from '@aztec/foundation/string';
13
+ import { bufferToHex, withHexPrefix } from '@aztec/foundation/string';
13
14
 
14
15
  import {
15
16
  type TransactionSerializable,
@@ -104,6 +105,82 @@ export class RemoteSigner implements EthSigner {
104
105
  private fetch: typeof globalThis.fetch = globalThis.fetch,
105
106
  ) {}
106
107
 
108
+ /**
109
+ * Validates that a web3signer is accessible and that the given addresses are available.
110
+ * @param remoteSignerUrl - The URL of the web3signer (can be string or EthRemoteSignerConfig)
111
+ * @param addresses - The addresses to check for availability
112
+ * @param fetch - Optional fetch implementation for testing
113
+ * @throws Error if the web3signer is not accessible or if any address is not available
114
+ */
115
+ static async validateAccess(
116
+ remoteSignerUrl: EthRemoteSignerConfig,
117
+ addresses: string[],
118
+ fetch: typeof globalThis.fetch = globalThis.fetch,
119
+ ): Promise<void> {
120
+ const url = typeof remoteSignerUrl === 'string' ? remoteSignerUrl : remoteSignerUrl.remoteSignerUrl;
121
+
122
+ try {
123
+ // Check if the web3signer is reachable by calling eth_accounts
124
+ const response = await fetch(url, {
125
+ method: 'POST',
126
+ headers: {
127
+ 'Content-Type': 'application/json',
128
+ },
129
+ body: JSON.stringify({
130
+ jsonrpc: '2.0',
131
+ method: 'eth_accounts',
132
+ params: [],
133
+ id: 1,
134
+ }),
135
+ });
136
+
137
+ if (!response.ok) {
138
+ const errorText = await response.text();
139
+ throw new SignerError(
140
+ `Web3Signer validation failed: ${response.status} ${response.statusText} - ${errorText}`,
141
+ 'eth_accounts',
142
+ url,
143
+ response.status,
144
+ );
145
+ }
146
+
147
+ const result = await response.json();
148
+
149
+ if (result.error) {
150
+ throw new SignerError(
151
+ `Web3Signer JSON-RPC error during validation: ${result.error.code} - ${result.error.message}`,
152
+ 'eth_accounts',
153
+ url,
154
+ 200,
155
+ result.error.code,
156
+ );
157
+ }
158
+
159
+ if (!result.result || !Array.isArray(result.result)) {
160
+ throw new Error('Invalid response from Web3Signer: expected array of accounts');
161
+ }
162
+
163
+ // Normalize addresses to lowercase for comparison
164
+ const availableAccounts: string[] = result.result.map((addr: string) => addr.toLowerCase());
165
+ const requestedAddresses = addresses.map(addr => addr.toLowerCase());
166
+
167
+ // Check if all requested addresses are available
168
+ const missingAddresses = requestedAddresses.filter(addr => !availableAccounts.includes(addr));
169
+
170
+ if (missingAddresses.length > 0) {
171
+ throw new Error(`The following addresses are not available in the web3signer: ${missingAddresses.join(', ')}`);
172
+ }
173
+ } catch (error: any) {
174
+ if (error instanceof SignerError) {
175
+ throw error;
176
+ }
177
+ if (error.code === 'ECONNREFUSED' || error.cause?.code === 'ECONNREFUSED') {
178
+ throw new Error(`Unable to connect to web3signer at ${url}. Please ensure it is running and accessible.`);
179
+ }
180
+ throw error;
181
+ }
182
+ }
183
+
107
184
  /**
108
185
  * Sign a message using eth_sign via remote JSON-RPC.
109
186
  */
@@ -167,10 +244,6 @@ export class RemoteSigner implements EthSigner {
167
244
  * Make a JSON-RPC eth_signTransaction request.
168
245
  */
169
246
  private async makeJsonRpcSignTransactionRequest(tx: TransactionSerializable): Promise<Signature> {
170
- if (tx.type !== 'eip1559') {
171
- throw new Error('This signer does not support tx type: ' + tx.type);
172
- }
173
-
174
247
  const txObject: RemoteSignerTxObject = {
175
248
  from: this.address.toString(),
176
249
  to: tx.to ?? null,
@@ -184,10 +257,10 @@ export class RemoteSigner implements EthSigner {
184
257
  ? withHexPrefix(tx.maxPriorityFeePerGas.toString(16))
185
258
  : undefined,
186
259
 
187
- // maxFeePerBlobGas:
188
- // typeof tx.maxFeePerBlobGas !== 'undefined' ? withHexPrefix(tx.maxFeePerBlobGas.toString(16)) : undefined,
189
- // blobVersionedHashes: tx.blobVersionedHashes,
190
- // blobs: tx.blobs?.map(blob => (typeof blob === 'string' ? blob : bufferToHex(Buffer.from(blob)))),
260
+ maxFeePerBlobGas:
261
+ typeof tx.maxFeePerBlobGas !== 'undefined' ? withHexPrefix(tx.maxFeePerBlobGas.toString(16)) : undefined,
262
+ blobVersionedHashes: tx.blobVersionedHashes,
263
+ blobs: tx.blobs?.map(blob => (typeof blob === 'string' ? blob : bufferToHex(Buffer.from(blob)))),
191
264
  };
192
265
 
193
266
  let rawTxHex = await this.makeJsonRpcRequest('eth_signTransaction', txObject);
package/src/types.ts CHANGED
@@ -5,21 +5,23 @@
5
5
  * These types define the JSON structure for configuring validators, provers, and
6
6
  * their associated keys and addresses.
7
7
  */
8
+ import type { EthAddress } from '@aztec/foundation/eth-address';
9
+ import type { Hex } from '@aztec/foundation/string';
10
+ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
8
11
 
9
- /** Parameterized hex string type for specific byte lengths */
10
- export type Hex<TByteLength extends number> = `0x${string}` & { readonly _length: TByteLength };
11
-
12
- /** A json keystore config points to a local file with the encrypted private key, and may require a password for decrypting it */
13
- export type EthJsonKeyFileV3Config = { path: string; password?: string };
12
+ /**
13
+ * An encrypted keystore file config points to a local file with an encrypted private key.
14
+ * The file may be in different formats:
15
+ * - JSON V3 format for ETH keys (Ethereum wallet standard)
16
+ * - EIP-2335 format for BLS keys (Ethereum 2.0 validator standard)
17
+ */
18
+ export type EncryptedKeyFileConfig = { path: string; password?: string };
14
19
 
15
20
  /** A private key is a 32-byte 0x-prefixed hex */
16
21
  export type EthPrivateKey = Hex<32>;
17
22
 
18
- /** An address is a 20-byte 0x-prefixed hex */
19
- export type EthAddressHex = Hex<20>;
20
-
21
- /** An Aztec address is a 32-byte 0x-prefixed hex */
22
- export type AztecAddressHex = Hex<32>;
23
+ /** A BLS private key is a 32-byte 0x-prefixed hex */
24
+ export type BLSPrivateKey = Hex<32>;
23
25
 
24
26
  /** URL type for remote signers */
25
27
  export type Url = string;
@@ -40,19 +42,19 @@ export type EthRemoteSignerConfig =
40
42
  * If only the address is set, then the default remote signer config from the parent config is used.
41
43
  */
42
44
  export type EthRemoteSignerAccount =
43
- | EthAddressHex
45
+ | EthAddress
44
46
  | {
45
- address: EthAddressHex;
46
- remoteSignerUrl?: Url;
47
+ address: EthAddress;
48
+ remoteSignerUrl: Url;
47
49
  certPath?: string;
48
50
  certPass?: string;
49
51
  };
50
52
 
51
- /** An L1 account is a private key, a remote signer configuration, or a standard json key store file */
52
- export type EthAccount = EthPrivateKey | EthRemoteSignerAccount | EthJsonKeyFileV3Config;
53
+ /** An L1 account is a private key, a remote signer configuration, or an encrypted keystore file (JSON V3 format) */
54
+ export type EthAccount = EthPrivateKey | EthRemoteSignerAccount | EncryptedKeyFileConfig;
53
55
 
54
56
  /** A mnemonic can be used to define a set of accounts */
55
- export type EthMnemonicConfig = {
57
+ export type MnemonicConfig = {
56
58
  mnemonic: string;
57
59
  addressIndex?: number;
58
60
  accountIndex?: number;
@@ -61,37 +63,47 @@ export type EthMnemonicConfig = {
61
63
  };
62
64
 
63
65
  /** One or more L1 accounts */
64
- export type EthAccounts = EthAccount | EthAccount[] | EthMnemonicConfig;
66
+ export type EthAccounts = EthAccount | EthAccount[] | MnemonicConfig;
65
67
 
66
- export type ProverKeyStore =
67
- | {
68
- /** Address that identifies the prover. This address will receive the rewards. */
69
- id: EthAddressHex;
70
- /** One or more EOAs used for sending proof L1 txs. */
71
- publisher: EthAccounts;
72
- }
73
- | EthAccount;
68
+ export type ProverKeyStoreWithId = {
69
+ /** Address that identifies the prover. This address will receive the rewards. */
70
+ id: EthAddress;
71
+ /** One or more EOAs used for sending proof L1 txs. */
72
+ publisher: EthAccounts;
73
+ };
74
+
75
+ export type ProverKeyStore = ProverKeyStoreWithId | EthAccount;
76
+
77
+ /** A BLS account is either a private key, or an EIP-2335 encrypted keystore file */
78
+ export type BLSAccount = BLSPrivateKey | EncryptedKeyFileConfig;
79
+
80
+ /** An AttesterAccount is a combined EthAccount and optional BLSAccount */
81
+ export type AttesterAccount = { eth: EthAccount; bls?: BLSAccount } | EthAccount;
82
+
83
+ /** One or more attester accounts combining ETH and BLS keys */
84
+ export type AttesterAccounts = AttesterAccount | AttesterAccount[] | MnemonicConfig;
74
85
 
75
86
  export type ValidatorKeyStore = {
76
87
  /**
77
88
  * One or more validator attester keys to handle in this configuration block.
78
89
  * An attester address may only appear once across all configuration blocks across all keystore files.
79
90
  */
80
- attester: EthAccounts;
91
+ attester: AttesterAccounts;
81
92
  /**
82
93
  * Coinbase address to use when proposing an L2 block as any of the validators in this configuration block.
83
- * Falls back to the attester address if not set.
94
+ * Falls back to the keystore-level coinbase, then to the attester address if not set.
84
95
  */
85
- coinbase?: EthAddressHex;
96
+ coinbase?: EthAddress;
86
97
  /**
87
98
  * One or more EOAs used for sending block proposal L1 txs for all validators in this configuration block.
88
- * Falls back to the attester account if not set.
99
+ * Falls back to the keystore-level publisher, then to the attester account if not set.
89
100
  */
90
101
  publisher?: EthAccounts;
91
102
  /**
92
103
  * Fee recipient address to use when proposing an L2 block as any of the validators in this configuration block.
104
+ * Falls back to the keystore-level feeRecipient if not set.
93
105
  */
94
- feeRecipient: AztecAddressHex;
106
+ feeRecipient?: AztecAddress;
95
107
  /**
96
108
  * Default remote signer for all accounts in this block.
97
109
  */
@@ -103,8 +115,8 @@ export type ValidatorKeyStore = {
103
115
  };
104
116
 
105
117
  export type KeyStore = {
106
- /** Schema version of this keystore file (initially 1). */
107
- schemaVersion: number;
118
+ /** Schema version of this keystore file (1 or 2). */
119
+ schemaVersion: 1 | 2;
108
120
  /** Validator configurations. */
109
121
  validators?: ValidatorKeyStore[];
110
122
  /** One or more accounts used for creating slash payloads on L1. Does not create slash payloads if not set. */
@@ -115,4 +127,10 @@ export type KeyStore = {
115
127
  prover?: ProverKeyStore;
116
128
  /** Used for automatically funding publisher accounts if there is none defined in the corresponding ValidatorKeyStore*/
117
129
  fundingAccount?: EthAccount;
130
+ /** Default publisher accounts for all validators in this keystore. Can be overridden by individual validator configs. */
131
+ publisher?: EthAccounts;
132
+ /** Default coinbase address for all validators in this keystore. Can be overridden by individual validator configs. */
133
+ coinbase?: EthAddress;
134
+ /** Default fee recipient address for all validators in this keystore. Can be overridden by individual validator configs. */
135
+ feeRecipient?: AztecAddress;
118
136
  };