@aztec/node-keystore 2.0.0-nightly.20250816

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.
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './loader.js';
3
+ export * from './schemas.js';
4
+ export * from './signer.js';
5
+ export * from './keystore_manager.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC"}
package/dest/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './loader.js';
3
+ export * from './schemas.js';
4
+ export * from './signer.js';
5
+ export * from './keystore_manager.js';
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Keystore Manager
3
+ *
4
+ * Manages keystore configuration and delegates signing operations to appropriate signers.
5
+ */
6
+ import { Buffer32 } from '@aztec/foundation/buffer';
7
+ import { EthAddress } from '@aztec/foundation/eth-address';
8
+ import type { Signature } from '@aztec/foundation/eth-signature';
9
+ import type { TypedDataDefinition } from 'viem';
10
+ import { type Signer } from './signer.js';
11
+ import type { EthAccounts, EthRemoteSignerConfig, KeyStore, ProverKeyStore, ValidatorKeyStore as ValidatorKeystoreConfig } from './types.js';
12
+ /**
13
+ * Error thrown when keystore operations fail
14
+ */
15
+ export declare class KeystoreError extends Error {
16
+ cause?: Error | undefined;
17
+ constructor(message: string, cause?: Error | undefined);
18
+ }
19
+ /**
20
+ * Keystore Manager - coordinates signing operations based on keystore configuration
21
+ */
22
+ export declare class KeystoreManager {
23
+ private readonly keystore;
24
+ /**
25
+ * Create a keystore manager from a parsed configuration.
26
+ * Performs a lightweight duplicate-attester check without decrypting JSON V3 or deriving mnemonics.
27
+ * @param keystore Parsed keystore configuration
28
+ */
29
+ constructor(keystore: KeyStore);
30
+ /**
31
+ * Validates that attester addresses are unique across all validators
32
+ * Only checks simple private key attesters, not JSON-V3 or mnemonic attesters,
33
+ * these are validated when decrypting the JSON-V3 keystore files
34
+ * @throws KeystoreError if duplicate attester addresses are found
35
+ */
36
+ private validateUniqueAttesterAddresses;
37
+ /**
38
+ * Best-effort address extraction that avoids decryption/derivation (no JSON-V3 or mnemonic processing).
39
+ * This is used at construction time to check for obvious duplicates without throwing for invalid inputs.
40
+ */
41
+ private extractAddressesWithoutSensitiveOperations;
42
+ /**
43
+ * Create signers for validator attester accounts
44
+ */
45
+ createAttesterSigners(validatorIndex: number): Signer[];
46
+ /**
47
+ * Create signers for validator publisher accounts (falls back to attester if not specified)
48
+ */
49
+ createPublisherSigners(validatorIndex: number): Signer[];
50
+ /**
51
+ * Create signers for slasher accounts
52
+ */
53
+ createSlasherSigners(): Signer[];
54
+ /**
55
+ * Create signers for prover accounts
56
+ */
57
+ createProverSigners(): Signer[];
58
+ /**
59
+ * Get validator configuration by index
60
+ */
61
+ getValidator(index: number): ValidatorKeystoreConfig;
62
+ /**
63
+ * Get validator count
64
+ */
65
+ getValidatorCount(): number;
66
+ /**
67
+ * Get coinbase address for validator (falls back to first attester address)
68
+ */
69
+ getCoinbaseAddress(validatorIndex: number): EthAddress;
70
+ /**
71
+ * Get fee recipient for validator
72
+ */
73
+ getFeeRecipient(validatorIndex: number): string;
74
+ /**
75
+ * Get the raw slasher configuration as provided in the keystore file.
76
+ * @returns The slasher accounts configuration or undefined if not set
77
+ */
78
+ getSlasherAccounts(): EthAccounts | undefined;
79
+ /**
80
+ * Get the raw prover configuration as provided in the keystore file.
81
+ * @returns The prover configuration or undefined if not set
82
+ */
83
+ getProverConfig(): ProverKeyStore | undefined;
84
+ /**
85
+ * Resolves attester accounts (including JSON V3 and mnemonic) and checks for duplicate addresses across validators.
86
+ * Throws if the same resolved address appears in more than one validator configuration.
87
+ */
88
+ validateResolvedUniqueAttesterAddresses(): void;
89
+ /**
90
+ * Create signers from EthAccounts configuration
91
+ */
92
+ private createSignersFromEthAccounts;
93
+ /**
94
+ * Create a signer from a single EthAccount configuration
95
+ */
96
+ private createSignerFromEthAccount;
97
+ /**
98
+ * Create signer from JSON V3 keystore file or directory
99
+ */
100
+ private createSignerFromJsonV3;
101
+ /**
102
+ * Create signer from a single JSON V3 keystore file
103
+ */
104
+ private createSignerFromSingleJsonV3File;
105
+ /**
106
+ * Create signers from mnemonic configuration using BIP44 derivation
107
+ */
108
+ private createSignersFromMnemonic;
109
+ /**
110
+ * Sign message with a specific signer
111
+ */
112
+ signMessage(signer: Signer, message: Buffer32): Promise<Signature>;
113
+ /**
114
+ * Sign typed data with a specific signer
115
+ */
116
+ signTypedData(signer: Signer, typedData: TypedDataDefinition): Promise<Signature>;
117
+ /**
118
+ * Get the effective remote signer configuration for a specific attester address
119
+ * Precedence: account-level override > validator-level config > file-level default
120
+ */
121
+ getEffectiveRemoteSignerConfig(validatorIndex: number, attesterAddress: EthAddress): EthRemoteSignerConfig | undefined;
122
+ }
123
+ //# sourceMappingURL=keystore_manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keystore_manager.d.ts","sourceRoot":"","sources":["../src/keystore_manager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAKjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAGhD,OAAO,EAA6B,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,KAAK,EAEV,WAAW,EAKX,qBAAqB,EACrB,QAAQ,EACR,cAAc,EACd,iBAAiB,IAAI,uBAAuB,EAC7C,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IAGpB,KAAK,CAAC,EAAE,KAAK;gBAD7B,OAAO,EAAE,MAAM,EACC,KAAK,CAAC,EAAE,KAAK,YAAA;CAKhC;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IAEpC;;;;OAIG;gBACS,QAAQ,EAAE,QAAQ;IAK9B;;;;;OAKG;IACH,OAAO,CAAC,+BAA+B;IAkBvC;;;OAGG;IACH,OAAO,CAAC,0CAA0C;IAiElD;;OAEG;IACH,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE;IAKvD;;OAEG;IACH,sBAAsB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE;IAcxD;;OAEG;IACH,oBAAoB,IAAI,MAAM,EAAE;IAQhC;;OAEG;IACH,mBAAmB,IAAI,MAAM,EAAE;IA0B/B;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,uBAAuB;IAOpD;;OAEG;IACH,iBAAiB,IAAI,MAAM;IAI3B;;OAEG;IACH,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,UAAU;IAgBtD;;OAEG;IACH,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM;IAK/C;;;OAGG;IACH,kBAAkB,IAAI,WAAW,GAAG,SAAS;IAI7C;;;OAGG;IACH,eAAe,IAAI,cAAc,GAAG,SAAS;IAI7C;;;OAGG;IACH,uCAAuC,IAAI,IAAI;IAqB/C;;OAEG;IACH,OAAO,CAAC,4BAA4B;IA4BpC;;OAEG;IACH,OAAO,CAAC,0BAA0B;IA+ClC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAkD9B;;OAEG;IACH,OAAO,CAAC,gCAAgC;IAwBxC;;OAEG;IACH,OAAO,CAAC,yBAAyB;IA8BjC;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IAIxE;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,SAAS,CAAC;IAIvF;;;OAGG;IACH,8BAA8B,CAC5B,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,UAAU,GAC1B,qBAAqB,GAAG,SAAS;CAiIrC"}
@@ -0,0 +1,511 @@
1
+ /**
2
+ * Keystore Manager
3
+ *
4
+ * Manages keystore configuration and delegates signing operations to appropriate signers.
5
+ */ import { Buffer32 } from '@aztec/foundation/buffer';
6
+ import { EthAddress } from '@aztec/foundation/eth-address';
7
+ import { Wallet } from '@ethersproject/wallet';
8
+ import { readFileSync, readdirSync, statSync } from 'fs';
9
+ import { extname, join } from 'path';
10
+ import { mnemonicToAccount } from 'viem/accounts';
11
+ import { LocalSigner, RemoteSigner } from './signer.js';
12
+ /**
13
+ * Error thrown when keystore operations fail
14
+ */ export class KeystoreError extends Error {
15
+ cause;
16
+ constructor(message, cause){
17
+ super(message), this.cause = cause;
18
+ this.name = 'KeystoreError';
19
+ }
20
+ }
21
+ /**
22
+ * Keystore Manager - coordinates signing operations based on keystore configuration
23
+ */ export class KeystoreManager {
24
+ keystore;
25
+ /**
26
+ * Create a keystore manager from a parsed configuration.
27
+ * Performs a lightweight duplicate-attester check without decrypting JSON V3 or deriving mnemonics.
28
+ * @param keystore Parsed keystore configuration
29
+ */ constructor(keystore){
30
+ this.keystore = keystore;
31
+ this.validateUniqueAttesterAddresses();
32
+ }
33
+ /**
34
+ * Validates that attester addresses are unique across all validators
35
+ * Only checks simple private key attesters, not JSON-V3 or mnemonic attesters,
36
+ * these are validated when decrypting the JSON-V3 keystore files
37
+ * @throws KeystoreError if duplicate attester addresses are found
38
+ */ validateUniqueAttesterAddresses() {
39
+ const seenAddresses = new Set();
40
+ const validatorCount = this.getValidatorCount();
41
+ for(let validatorIndex = 0; validatorIndex < validatorCount; validatorIndex++){
42
+ const validator = this.getValidator(validatorIndex);
43
+ const addresses = this.extractAddressesWithoutSensitiveOperations(validator.attester);
44
+ for (const addr of addresses){
45
+ const address = addr.toString().toLowerCase();
46
+ if (seenAddresses.has(address)) {
47
+ throw new KeystoreError(`Duplicate attester address found: ${addr.toString()}. An attester address may only appear once across all configuration blocks.`);
48
+ }
49
+ seenAddresses.add(address);
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Best-effort address extraction that avoids decryption/derivation (no JSON-V3 or mnemonic processing).
55
+ * This is used at construction time to check for obvious duplicates without throwing for invalid inputs.
56
+ */ extractAddressesWithoutSensitiveOperations(accounts) {
57
+ const results = [];
58
+ const handleAccount = (account)=>{
59
+ // String cases: private key or address or remote signer address
60
+ if (typeof account === 'string') {
61
+ if (account.startsWith('0x') && account.length === 66) {
62
+ // Private key -> derive address locally without external deps
63
+ try {
64
+ const signer = new LocalSigner(Buffer32.fromString(account));
65
+ results.push(signer.address);
66
+ } catch {
67
+ // Ignore invalid private key at construction time
68
+ }
69
+ return;
70
+ }
71
+ if (account.startsWith('0x') && account.length === 42) {
72
+ // Address string
73
+ try {
74
+ results.push(EthAddress.fromString(account));
75
+ } catch {
76
+ // Ignore invalid address format at construction time
77
+ }
78
+ return;
79
+ }
80
+ // Any other string cannot be confidently resolved here
81
+ return;
82
+ }
83
+ // JSON V3 keystore: skip (requires decryption)
84
+ if ('path' in account) {
85
+ return;
86
+ }
87
+ // Mnemonic: skip (requires derivation and may throw on invalid mnemonics)
88
+ if ('mnemonic' in account) {
89
+ return;
90
+ }
91
+ // Remote signer account (object form)
92
+ const remoteSigner = account;
93
+ const address = typeof remoteSigner === 'string' ? remoteSigner : remoteSigner.address;
94
+ if (address) {
95
+ try {
96
+ results.push(EthAddress.fromString(address));
97
+ } catch {
98
+ // Ignore invalid address format at construction time
99
+ }
100
+ }
101
+ };
102
+ if (Array.isArray(accounts)) {
103
+ for (const account of accounts){
104
+ const subResults = this.extractAddressesWithoutSensitiveOperations(account);
105
+ results.push(...subResults);
106
+ }
107
+ return results;
108
+ }
109
+ handleAccount(accounts);
110
+ return results;
111
+ }
112
+ /**
113
+ * Create signers for validator attester accounts
114
+ */ createAttesterSigners(validatorIndex) {
115
+ const validator = this.getValidator(validatorIndex);
116
+ return this.createSignersFromEthAccounts(validator.attester, validator.remoteSigner || this.keystore.remoteSigner);
117
+ }
118
+ /**
119
+ * Create signers for validator publisher accounts (falls back to attester if not specified)
120
+ */ createPublisherSigners(validatorIndex) {
121
+ const validator = this.getValidator(validatorIndex);
122
+ if (validator.publisher) {
123
+ return this.createSignersFromEthAccounts(validator.publisher, validator.remoteSigner || this.keystore.remoteSigner);
124
+ }
125
+ // Fall back to attester signers
126
+ return this.createAttesterSigners(validatorIndex);
127
+ }
128
+ /**
129
+ * Create signers for slasher accounts
130
+ */ createSlasherSigners() {
131
+ if (!this.keystore.slasher) {
132
+ return [];
133
+ }
134
+ return this.createSignersFromEthAccounts(this.keystore.slasher, this.keystore.remoteSigner);
135
+ }
136
+ /**
137
+ * Create signers for prover accounts
138
+ */ createProverSigners() {
139
+ if (!this.keystore.prover) {
140
+ return [];
141
+ }
142
+ // Handle simple prover case (just a private key)
143
+ if (typeof this.keystore.prover === 'string' || 'path' in this.keystore.prover || 'address' in this.keystore.prover) {
144
+ return this.createSignersFromEthAccounts(this.keystore.prover, this.keystore.remoteSigner);
145
+ }
146
+ // Handle complex prover case with id and publishers
147
+ const proverConfig = this.keystore.prover;
148
+ const signers = [];
149
+ for (const publisherAccounts of proverConfig.publisher){
150
+ const publisherSigners = this.createSignersFromEthAccounts(publisherAccounts, this.keystore.remoteSigner);
151
+ signers.push(...publisherSigners);
152
+ }
153
+ return signers;
154
+ }
155
+ /**
156
+ * Get validator configuration by index
157
+ */ getValidator(index) {
158
+ if (!this.keystore.validators || index >= this.keystore.validators.length || index < 0) {
159
+ throw new KeystoreError(`Validator index ${index} out of bounds`);
160
+ }
161
+ return this.keystore.validators[index];
162
+ }
163
+ /**
164
+ * Get validator count
165
+ */ getValidatorCount() {
166
+ return this.keystore.validators?.length || 0;
167
+ }
168
+ /**
169
+ * Get coinbase address for validator (falls back to first attester address)
170
+ */ getCoinbaseAddress(validatorIndex) {
171
+ const validator = this.getValidator(validatorIndex);
172
+ if (validator.coinbase) {
173
+ return EthAddress.fromString(validator.coinbase);
174
+ }
175
+ // Fall back to first attester address
176
+ const attesterSigners = this.createAttesterSigners(validatorIndex);
177
+ if (attesterSigners.length === 0) {
178
+ throw new KeystoreError(`No attester signers found for validator ${validatorIndex}`);
179
+ }
180
+ return attesterSigners[0].address;
181
+ }
182
+ /**
183
+ * Get fee recipient for validator
184
+ */ getFeeRecipient(validatorIndex) {
185
+ const validator = this.getValidator(validatorIndex);
186
+ return validator.feeRecipient;
187
+ }
188
+ /**
189
+ * Get the raw slasher configuration as provided in the keystore file.
190
+ * @returns The slasher accounts configuration or undefined if not set
191
+ */ getSlasherAccounts() {
192
+ return this.keystore.slasher;
193
+ }
194
+ /**
195
+ * Get the raw prover configuration as provided in the keystore file.
196
+ * @returns The prover configuration or undefined if not set
197
+ */ getProverConfig() {
198
+ return this.keystore.prover;
199
+ }
200
+ /**
201
+ * Resolves attester accounts (including JSON V3 and mnemonic) and checks for duplicate addresses across validators.
202
+ * Throws if the same resolved address appears in more than one validator configuration.
203
+ */ validateResolvedUniqueAttesterAddresses() {
204
+ const seenAddresses = new Set();
205
+ const validatorCount = this.getValidatorCount();
206
+ for(let validatorIndex = 0; validatorIndex < validatorCount; validatorIndex++){
207
+ const validator = this.getValidator(validatorIndex);
208
+ const signers = this.createSignersFromEthAccounts(validator.attester, validator.remoteSigner || this.keystore.remoteSigner);
209
+ for (const signer of signers){
210
+ const address = signer.address.toString().toLowerCase();
211
+ if (seenAddresses.has(address)) {
212
+ throw new KeystoreError(`Duplicate attester address found after resolving accounts: ${address}. An attester address may only appear once across all configuration blocks.`);
213
+ }
214
+ seenAddresses.add(address);
215
+ }
216
+ }
217
+ }
218
+ /**
219
+ * Create signers from EthAccounts configuration
220
+ */ createSignersFromEthAccounts(accounts, defaultRemoteSigner) {
221
+ if (typeof accounts === 'string') {
222
+ return [
223
+ this.createSignerFromEthAccount(accounts, defaultRemoteSigner)
224
+ ];
225
+ }
226
+ if (Array.isArray(accounts)) {
227
+ const signers = [];
228
+ for (const account of accounts){
229
+ const accountSigners = this.createSignersFromEthAccounts(account, defaultRemoteSigner);
230
+ signers.push(...accountSigners);
231
+ }
232
+ return signers;
233
+ }
234
+ // Mnemonic configuration
235
+ if ('mnemonic' in accounts) {
236
+ return this.createSignersFromMnemonic(accounts);
237
+ }
238
+ // Single account object - handle JSON V3 directory case
239
+ if ('path' in accounts) {
240
+ const result = this.createSignerFromJsonV3(accounts);
241
+ return result;
242
+ }
243
+ return [
244
+ this.createSignerFromEthAccount(accounts, defaultRemoteSigner)
245
+ ];
246
+ }
247
+ /**
248
+ * Create a signer from a single EthAccount configuration
249
+ */ createSignerFromEthAccount(account, defaultRemoteSigner) {
250
+ // Private key (hex string)
251
+ if (typeof account === 'string') {
252
+ if (account.startsWith('0x') && account.length === 66) {
253
+ // Private key
254
+ return new LocalSigner(Buffer32.fromString(account));
255
+ } else {
256
+ // Remote signer address only - use default remote signer config
257
+ if (!defaultRemoteSigner) {
258
+ throw new KeystoreError(`No remote signer configuration found for address ${account}`);
259
+ }
260
+ return new RemoteSigner(EthAddress.fromString(account), defaultRemoteSigner);
261
+ }
262
+ }
263
+ // JSON V3 keystore
264
+ if ('path' in account) {
265
+ const result = this.createSignerFromJsonV3(account);
266
+ return result[0];
267
+ }
268
+ // Remote signer account
269
+ const remoteSigner = account;
270
+ if (typeof remoteSigner === 'string') {
271
+ // Just an address - use default config
272
+ if (!defaultRemoteSigner) {
273
+ throw new KeystoreError(`No remote signer configuration found for address ${remoteSigner}`);
274
+ }
275
+ return new RemoteSigner(EthAddress.fromString(remoteSigner), defaultRemoteSigner);
276
+ }
277
+ // Remote signer with config
278
+ const config = remoteSigner.remoteSignerUrl ? {
279
+ remoteSignerUrl: remoteSigner.remoteSignerUrl,
280
+ certPath: remoteSigner.certPath,
281
+ certPass: remoteSigner.certPass
282
+ } : defaultRemoteSigner;
283
+ if (!config) {
284
+ throw new KeystoreError(`No remote signer configuration found for address ${remoteSigner.address}`);
285
+ }
286
+ return new RemoteSigner(EthAddress.fromString(remoteSigner.address), config);
287
+ }
288
+ /**
289
+ * Create signer from JSON V3 keystore file or directory
290
+ */ createSignerFromJsonV3(config) {
291
+ try {
292
+ const stats = statSync(config.path);
293
+ if (stats.isDirectory()) {
294
+ // Handle directory - load all JSON files
295
+ const files = readdirSync(config.path);
296
+ const signers = [];
297
+ const seenAddresses = new Map(); // address -> file name
298
+ for (const file of files){
299
+ // Only process .json files
300
+ if (extname(file).toLowerCase() !== '.json') {
301
+ continue;
302
+ }
303
+ const filePath = join(config.path, file);
304
+ try {
305
+ const signer = this.createSignerFromSingleJsonV3File(filePath, config.password);
306
+ const addressString = signer.address.toString().toLowerCase();
307
+ const existingFile = seenAddresses.get(addressString);
308
+ if (existingFile) {
309
+ throw new KeystoreError(`Duplicate JSON V3 keystore address ${addressString} found in directory ${config.path} (files: ${existingFile} and ${file}). Each keystore must have a unique address.`);
310
+ }
311
+ seenAddresses.set(addressString, file);
312
+ signers.push(signer);
313
+ } catch (error) {
314
+ // Re-throw with file context
315
+ throw new KeystoreError(`Failed to load keystore file ${file}: ${error}`, error);
316
+ }
317
+ }
318
+ if (signers.length === 0) {
319
+ throw new KeystoreError(`No JSON keystore files found in directory ${config.path}`);
320
+ }
321
+ return signers;
322
+ } else {
323
+ // Single file
324
+ return [
325
+ this.createSignerFromSingleJsonV3File(config.path, config.password)
326
+ ];
327
+ }
328
+ } catch (error) {
329
+ if (error instanceof KeystoreError) {
330
+ throw error;
331
+ }
332
+ throw new KeystoreError(`Failed to access JSON V3 keystore ${config.path}: ${error}`, error);
333
+ }
334
+ }
335
+ /**
336
+ * Create signer from a single JSON V3 keystore file
337
+ */ createSignerFromSingleJsonV3File(filePath, password) {
338
+ try {
339
+ // Read the keystore file
340
+ const keystoreJson = readFileSync(filePath, 'utf8');
341
+ // Get password - prompt for it if not provided
342
+ const resolvedPassword = password;
343
+ if (!resolvedPassword) {
344
+ throw new KeystoreError(`No password provided for keystore ${filePath}. Provide password in config.`);
345
+ }
346
+ // Use @ethersproject/wallet to decrypt the JSON V3 keystore synchronously
347
+ const ethersWallet = Wallet.fromEncryptedJsonSync(keystoreJson, resolvedPassword);
348
+ // Convert the private key to our format
349
+ const privateKey = Buffer32.fromString(ethersWallet.privateKey);
350
+ return new LocalSigner(privateKey);
351
+ } catch (error) {
352
+ const err = error;
353
+ throw new KeystoreError(`Failed to decrypt JSON V3 keystore ${filePath}: ${err.message}`, err);
354
+ }
355
+ }
356
+ /**
357
+ * Create signers from mnemonic configuration using BIP44 derivation
358
+ */ createSignersFromMnemonic(config) {
359
+ const { mnemonic, addressIndex = 0, accountIndex = 0, addressCount = 1, accountCount = 1 } = config;
360
+ const signers = [];
361
+ try {
362
+ // Use viem's mnemonic derivation (imported at top of file)
363
+ // Normalize mnemonic by trimming whitespace
364
+ const normalizedMnemonic = mnemonic.trim();
365
+ for(let accIdx = accountIndex; accIdx < accountIndex + accountCount; accIdx++){
366
+ for(let addrIdx = addressIndex; addrIdx < addressIndex + addressCount; addrIdx++){
367
+ const viemAccount = mnemonicToAccount(normalizedMnemonic, {
368
+ accountIndex: accIdx,
369
+ addressIndex: addrIdx
370
+ });
371
+ // Extract the private key from the viem account
372
+ const privateKeyBytes = viemAccount.getHdKey().privateKey;
373
+ const privateKey = Buffer32.fromBuffer(Buffer.from(privateKeyBytes));
374
+ signers.push(new LocalSigner(privateKey));
375
+ }
376
+ }
377
+ return signers;
378
+ } catch (error) {
379
+ throw new KeystoreError(`Failed to derive accounts from mnemonic: ${error}`, error);
380
+ }
381
+ }
382
+ /**
383
+ * Sign message with a specific signer
384
+ */ async signMessage(signer, message) {
385
+ return await signer.signMessage(message);
386
+ }
387
+ /**
388
+ * Sign typed data with a specific signer
389
+ */ async signTypedData(signer, typedData) {
390
+ return await signer.signTypedData(typedData);
391
+ }
392
+ /**
393
+ * Get the effective remote signer configuration for a specific attester address
394
+ * Precedence: account-level override > validator-level config > file-level default
395
+ */ getEffectiveRemoteSignerConfig(validatorIndex, attesterAddress) {
396
+ const validator = this.getValidator(validatorIndex);
397
+ // Helper to get address from an account configuration
398
+ const getAddressFromAccount = (account)=>{
399
+ if (typeof account === 'string') {
400
+ if (account.startsWith('0x') && account.length === 66) {
401
+ // This is a private key - derive the address
402
+ try {
403
+ const signer = new LocalSigner(Buffer32.fromString(account));
404
+ return signer.address;
405
+ } catch {
406
+ return null;
407
+ }
408
+ } else if (account.startsWith('0x') && account.length === 42) {
409
+ // This is an address
410
+ try {
411
+ return EthAddress.fromString(account);
412
+ } catch {
413
+ return null;
414
+ }
415
+ }
416
+ return null;
417
+ }
418
+ // JSON V3 keystore
419
+ if ('path' in account) {
420
+ try {
421
+ const signers = this.createSignerFromJsonV3(account);
422
+ return signers.map((s)=>s.address);
423
+ } catch {
424
+ return null;
425
+ }
426
+ }
427
+ // Remote signer account
428
+ const remoteSigner = account;
429
+ const address = typeof remoteSigner === 'string' ? remoteSigner : remoteSigner.address;
430
+ try {
431
+ return EthAddress.fromString(address);
432
+ } catch {
433
+ return null;
434
+ }
435
+ };
436
+ // Helper to check if account matches and get its remote signer config
437
+ const checkAccount = (account)=>{
438
+ const addresses = getAddressFromAccount(account);
439
+ if (!addresses) {
440
+ return undefined;
441
+ }
442
+ const addressArray = Array.isArray(addresses) ? addresses : [
443
+ addresses
444
+ ];
445
+ const matches = addressArray.some((addr)=>addr.equals(attesterAddress));
446
+ if (!matches) {
447
+ return undefined;
448
+ }
449
+ // Found a match - determine the config to return
450
+ if (typeof account === 'string') {
451
+ if (account.startsWith('0x') && account.length === 66) {
452
+ // Private key - local signer, no remote config
453
+ return undefined;
454
+ } else {
455
+ // Address only - use defaults
456
+ return validator.remoteSigner || this.keystore.remoteSigner;
457
+ }
458
+ }
459
+ // JSON V3 - local signer, no remote config
460
+ if ('path' in account) {
461
+ return undefined;
462
+ }
463
+ // Remote signer account with potential override
464
+ const remoteSigner = account;
465
+ if (typeof remoteSigner === 'string') {
466
+ // Just an address - use defaults
467
+ return validator.remoteSigner || this.keystore.remoteSigner;
468
+ }
469
+ // Has inline config
470
+ if (remoteSigner.remoteSignerUrl) {
471
+ return {
472
+ remoteSignerUrl: remoteSigner.remoteSignerUrl,
473
+ certPath: remoteSigner.certPath,
474
+ certPass: remoteSigner.certPass
475
+ };
476
+ } else {
477
+ // No URL specified, use defaults
478
+ return validator.remoteSigner || this.keystore.remoteSigner;
479
+ }
480
+ };
481
+ // Check the attester configuration
482
+ const { attester } = validator;
483
+ if (typeof attester === 'string') {
484
+ const result = checkAccount(attester);
485
+ return result === undefined ? undefined : result;
486
+ }
487
+ if (Array.isArray(attester)) {
488
+ for (const account of attester){
489
+ const result = checkAccount(account);
490
+ if (result !== undefined) {
491
+ return result;
492
+ }
493
+ }
494
+ return undefined;
495
+ }
496
+ // Mnemonic configuration
497
+ if ('mnemonic' in attester) {
498
+ try {
499
+ const signers = this.createSignersFromMnemonic(attester);
500
+ const matches = signers.some((s)=>s.address.equals(attesterAddress));
501
+ // Mnemonic-derived keys are local signers
502
+ return matches ? undefined : undefined;
503
+ } catch {
504
+ return undefined;
505
+ }
506
+ }
507
+ // Single account object
508
+ const result = checkAccount(attester);
509
+ return result === undefined ? undefined : result;
510
+ }
511
+ }