@aztec/node-keystore 3.0.0-canary.a9708bd → 3.0.0-devnet.2-patch.1
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/config.d.ts +1 -1
- package/dest/index.d.ts +1 -1
- package/dest/keystore_manager.d.ts +17 -5
- package/dest/keystore_manager.d.ts.map +1 -1
- package/dest/keystore_manager.js +179 -121
- package/dest/loader.d.ts +1 -1
- package/dest/loader.d.ts.map +1 -1
- package/dest/loader.js +42 -15
- package/dest/schemas.d.ts +976 -783
- package/dest/schemas.d.ts.map +1 -1
- package/dest/schemas.js +54 -34
- package/dest/signer.d.ts +10 -17
- package/dest/signer.d.ts.map +1 -1
- package/dest/signer.js +58 -6
- package/dest/types.d.ts +41 -23
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +1 -1
- package/package.json +8 -7
- package/src/keystore_manager.ts +216 -144
- package/src/loader.ts +38 -9
- package/src/schemas.ts +83 -53
- package/src/signer.ts +84 -11
- package/src/types.ts +45 -28
package/src/keystore_manager.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Manages keystore configuration and delegates signing operations to appropriate signers.
|
|
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
8
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
9
9
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
10
|
+
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
10
11
|
|
|
11
12
|
import { Wallet } from '@ethersproject/wallet';
|
|
12
13
|
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
@@ -14,16 +15,17 @@ import { extname, join } from 'path';
|
|
|
14
15
|
import type { TypedDataDefinition } from 'viem';
|
|
15
16
|
import { mnemonicToAccount } from 'viem/accounts';
|
|
16
17
|
|
|
18
|
+
import { ethPrivateKeySchema } from './schemas.js';
|
|
17
19
|
import { LocalSigner, RemoteSigner } from './signer.js';
|
|
18
20
|
import type {
|
|
21
|
+
AttesterAccounts,
|
|
22
|
+
EncryptedKeyFileConfig,
|
|
19
23
|
EthAccount,
|
|
20
24
|
EthAccounts,
|
|
21
|
-
EthJsonKeyFileV3Config,
|
|
22
|
-
EthMnemonicConfig,
|
|
23
|
-
EthPrivateKey,
|
|
24
25
|
EthRemoteSignerAccount,
|
|
25
26
|
EthRemoteSignerConfig,
|
|
26
27
|
KeyStore,
|
|
28
|
+
MnemonicConfig,
|
|
27
29
|
ProverKeyStore,
|
|
28
30
|
ValidatorKeyStore as ValidatorKeystoreConfig,
|
|
29
31
|
} from './types.js';
|
|
@@ -57,6 +59,82 @@ export class KeystoreManager {
|
|
|
57
59
|
this.validateUniqueAttesterAddresses();
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Validates all remote signers in the keystore are accessible and have the required addresses.
|
|
64
|
+
* Should be called after construction if validation is needed.
|
|
65
|
+
*/
|
|
66
|
+
async validateSigners(): Promise<void> {
|
|
67
|
+
// Collect all remote signers with their addresses grouped by URL
|
|
68
|
+
const remoteSignersByUrl = new Map<string, Set<string>>();
|
|
69
|
+
|
|
70
|
+
// Helper to extract remote signer URL from config
|
|
71
|
+
const getUrl = (config: EthRemoteSignerConfig): string => {
|
|
72
|
+
return typeof config === 'string' ? config : config.remoteSignerUrl;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Helper to collect remote signers from accounts
|
|
76
|
+
const collectRemoteSigners = (accounts: EthAccounts, defaultRemoteSigner?: EthRemoteSignerConfig): void => {
|
|
77
|
+
const processAccount = (account: EthAccount): void => {
|
|
78
|
+
if (typeof account === 'object' && !('path' in account) && !('mnemonic' in (account as any))) {
|
|
79
|
+
// This is a remote signer account
|
|
80
|
+
const remoteSigner = account as EthRemoteSignerAccount;
|
|
81
|
+
const address = 'address' in remoteSigner ? remoteSigner.address : remoteSigner;
|
|
82
|
+
|
|
83
|
+
let url: string;
|
|
84
|
+
if ('remoteSignerUrl' in remoteSigner && remoteSigner.remoteSignerUrl) {
|
|
85
|
+
url = remoteSigner.remoteSignerUrl;
|
|
86
|
+
} else if (defaultRemoteSigner) {
|
|
87
|
+
url = getUrl(defaultRemoteSigner);
|
|
88
|
+
} else {
|
|
89
|
+
return; // No remote signer URL available
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!remoteSignersByUrl.has(url)) {
|
|
93
|
+
remoteSignersByUrl.set(url, new Set());
|
|
94
|
+
}
|
|
95
|
+
remoteSignersByUrl.get(url)!.add(address.toString());
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(accounts)) {
|
|
100
|
+
accounts.forEach(account => collectRemoteSigners(account, defaultRemoteSigner));
|
|
101
|
+
} else if (typeof accounts === 'object' && 'mnemonic' in accounts) {
|
|
102
|
+
// Skip mnemonic configs
|
|
103
|
+
} else {
|
|
104
|
+
processAccount(accounts as EthAccount);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Collect from validators
|
|
109
|
+
const validatorCount = this.getValidatorCount();
|
|
110
|
+
for (let i = 0; i < validatorCount; i++) {
|
|
111
|
+
const validator = this.getValidator(i);
|
|
112
|
+
const remoteSigner = validator.remoteSigner || this.keystore.remoteSigner;
|
|
113
|
+
|
|
114
|
+
collectRemoteSigners(this.extractEthAccountsFromAttester(validator.attester), remoteSigner);
|
|
115
|
+
if (validator.publisher) {
|
|
116
|
+
collectRemoteSigners(validator.publisher, remoteSigner);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Collect from slasher
|
|
121
|
+
if (this.keystore.slasher) {
|
|
122
|
+
collectRemoteSigners(this.keystore.slasher, this.keystore.remoteSigner);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Collect from prover
|
|
126
|
+
if (this.keystore.prover && typeof this.keystore.prover === 'object' && 'publisher' in this.keystore.prover) {
|
|
127
|
+
collectRemoteSigners(this.keystore.prover.publisher, this.keystore.remoteSigner);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Validate each remote signer URL with all its addresses
|
|
131
|
+
for (const [url, addresses] of remoteSignersByUrl.entries()) {
|
|
132
|
+
if (addresses.size > 0) {
|
|
133
|
+
await RemoteSigner.validateAccess(url, Array.from(addresses));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
60
138
|
/**
|
|
61
139
|
* Validates that attester addresses are unique across all validators
|
|
62
140
|
* Only checks simple private key attesters, not JSON-V3 or mnemonic attesters,
|
|
@@ -85,67 +163,57 @@ export class KeystoreManager {
|
|
|
85
163
|
* Best-effort address extraction that avoids decryption/derivation (no JSON-V3 or mnemonic processing).
|
|
86
164
|
* This is used at construction time to check for obvious duplicates without throwing for invalid inputs.
|
|
87
165
|
*/
|
|
88
|
-
private extractAddressesWithoutSensitiveOperations(accounts:
|
|
166
|
+
private extractAddressesWithoutSensitiveOperations(accounts: AttesterAccounts): EthAddress[] {
|
|
167
|
+
const ethAccounts = this.extractEthAccountsFromAttester(accounts);
|
|
168
|
+
return this.extractAddressesFromEthAccountsNonSensitive(ethAccounts);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extract addresses from EthAccounts without sensitive operations (no decryption/derivation).
|
|
173
|
+
*/
|
|
174
|
+
private extractAddressesFromEthAccountsNonSensitive(accounts: EthAccounts): EthAddress[] {
|
|
89
175
|
const results: EthAddress[] = [];
|
|
90
176
|
|
|
91
177
|
const handleAccount = (account: EthAccount): void => {
|
|
92
|
-
// String cases: private key or address or remote signer address
|
|
93
178
|
if (typeof account === 'string') {
|
|
94
179
|
if (account.startsWith('0x') && account.length === 66) {
|
|
95
|
-
// Private key -> derive address locally without external deps
|
|
96
180
|
try {
|
|
97
|
-
const signer = new LocalSigner(Buffer32.fromString(account
|
|
181
|
+
const signer = new LocalSigner(Buffer32.fromString(ethPrivateKeySchema.parse(account)));
|
|
98
182
|
results.push(signer.address);
|
|
99
183
|
} catch {
|
|
100
|
-
//
|
|
101
|
-
}
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (account.startsWith('0x') && account.length === 42) {
|
|
106
|
-
// Address string
|
|
107
|
-
try {
|
|
108
|
-
results.push(EthAddress.fromString(account));
|
|
109
|
-
} catch {
|
|
110
|
-
// Ignore invalid address format at construction time
|
|
184
|
+
// ignore invalid private key at construction time
|
|
111
185
|
}
|
|
112
|
-
return;
|
|
113
186
|
}
|
|
114
|
-
|
|
115
|
-
// Any other string cannot be confidently resolved here
|
|
116
187
|
return;
|
|
117
188
|
}
|
|
118
189
|
|
|
119
|
-
// JSON V3 keystore: skip (requires decryption)
|
|
120
190
|
if ('path' in account) {
|
|
121
191
|
return;
|
|
122
192
|
}
|
|
123
193
|
|
|
124
|
-
// Mnemonic: skip (requires derivation and may throw on invalid mnemonics)
|
|
125
194
|
if ('mnemonic' in (account as any)) {
|
|
126
195
|
return;
|
|
127
196
|
}
|
|
128
197
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
results.push(EthAddress.fromString(address));
|
|
135
|
-
} catch {
|
|
136
|
-
// Ignore invalid address format at construction time
|
|
137
|
-
}
|
|
198
|
+
const remoteSigner: EthRemoteSignerAccount = account;
|
|
199
|
+
if ('address' in remoteSigner) {
|
|
200
|
+
results.push(remoteSigner.address);
|
|
201
|
+
return;
|
|
138
202
|
}
|
|
203
|
+
results.push(remoteSigner);
|
|
139
204
|
};
|
|
140
205
|
|
|
141
206
|
if (Array.isArray(accounts)) {
|
|
142
207
|
for (const account of accounts) {
|
|
143
|
-
|
|
144
|
-
results.push(...subResults);
|
|
208
|
+
handleAccount(account as EthAccount);
|
|
145
209
|
}
|
|
146
210
|
return results;
|
|
147
211
|
}
|
|
148
212
|
|
|
213
|
+
if (typeof accounts === 'object' && accounts !== null && 'mnemonic' in (accounts as any)) {
|
|
214
|
+
return results;
|
|
215
|
+
}
|
|
216
|
+
|
|
149
217
|
handleAccount(accounts as EthAccount);
|
|
150
218
|
return results;
|
|
151
219
|
}
|
|
@@ -155,7 +223,8 @@ export class KeystoreManager {
|
|
|
155
223
|
*/
|
|
156
224
|
createAttesterSigners(validatorIndex: number): EthSigner[] {
|
|
157
225
|
const validator = this.getValidator(validatorIndex);
|
|
158
|
-
|
|
226
|
+
const ethAccounts = this.extractEthAccountsFromAttester(validator.attester);
|
|
227
|
+
return this.createSignersFromEthAccounts(ethAccounts, validator.remoteSigner || this.keystore.remoteSigner);
|
|
159
228
|
}
|
|
160
229
|
|
|
161
230
|
/**
|
|
@@ -205,7 +274,7 @@ export class KeystoreManager {
|
|
|
205
274
|
return undefined;
|
|
206
275
|
}
|
|
207
276
|
|
|
208
|
-
// Handle
|
|
277
|
+
// Handle prover being a private key, JSON key store or remote signer with nested address
|
|
209
278
|
if (
|
|
210
279
|
typeof this.keystore.prover === 'string' ||
|
|
211
280
|
'path' in this.keystore.prover ||
|
|
@@ -218,10 +287,19 @@ export class KeystoreManager {
|
|
|
218
287
|
};
|
|
219
288
|
}
|
|
220
289
|
|
|
221
|
-
|
|
222
|
-
|
|
290
|
+
// Handle prover as Id and specified publishers
|
|
291
|
+
if ('id' in this.keystore.prover) {
|
|
292
|
+
const id = this.keystore.prover.id;
|
|
293
|
+
const signers = this.createSignersFromEthAccounts(this.keystore.prover.publisher, this.keystore.remoteSigner);
|
|
294
|
+
return { id, signers };
|
|
295
|
+
}
|
|
223
296
|
|
|
224
|
-
|
|
297
|
+
// Here, prover is just an EthAddress for a remote signer
|
|
298
|
+
const signers = this.createSignersFromEthAccounts(this.keystore.prover, this.keystore.remoteSigner);
|
|
299
|
+
return {
|
|
300
|
+
id: undefined,
|
|
301
|
+
signers,
|
|
302
|
+
};
|
|
225
303
|
}
|
|
226
304
|
|
|
227
305
|
/**
|
|
@@ -242,28 +320,23 @@ export class KeystoreManager {
|
|
|
242
320
|
}
|
|
243
321
|
|
|
244
322
|
/**
|
|
245
|
-
* Get coinbase address for validator (falls back to
|
|
323
|
+
* Get coinbase address for validator (falls back to the specific attester address)
|
|
246
324
|
*/
|
|
247
|
-
getCoinbaseAddress(validatorIndex: number): EthAddress {
|
|
325
|
+
getCoinbaseAddress(validatorIndex: number, attesterAddress: EthAddress): EthAddress {
|
|
248
326
|
const validator = this.getValidator(validatorIndex);
|
|
249
327
|
|
|
250
328
|
if (validator.coinbase) {
|
|
251
|
-
return
|
|
329
|
+
return validator.coinbase;
|
|
252
330
|
}
|
|
253
331
|
|
|
254
|
-
// Fall back to
|
|
255
|
-
|
|
256
|
-
if (attesterSigners.length === 0) {
|
|
257
|
-
throw new KeystoreError(`No attester signers found for validator ${validatorIndex}`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return attesterSigners[0].address;
|
|
332
|
+
// Fall back to the specific attester address
|
|
333
|
+
return attesterAddress;
|
|
261
334
|
}
|
|
262
335
|
|
|
263
336
|
/**
|
|
264
337
|
* Get fee recipient for validator
|
|
265
338
|
*/
|
|
266
|
-
getFeeRecipient(validatorIndex: number):
|
|
339
|
+
getFeeRecipient(validatorIndex: number): AztecAddress {
|
|
267
340
|
const validator = this.getValidator(validatorIndex);
|
|
268
341
|
return validator.feeRecipient;
|
|
269
342
|
}
|
|
@@ -294,7 +367,7 @@ export class KeystoreManager {
|
|
|
294
367
|
for (let validatorIndex = 0; validatorIndex < validatorCount; validatorIndex++) {
|
|
295
368
|
const validator = this.getValidator(validatorIndex);
|
|
296
369
|
const signers = this.createSignersFromEthAccounts(
|
|
297
|
-
validator.attester,
|
|
370
|
+
this.extractEthAccountsFromAttester(validator.attester),
|
|
298
371
|
validator.remoteSigner || this.keystore.remoteSigner,
|
|
299
372
|
);
|
|
300
373
|
for (const signer of signers) {
|
|
@@ -351,13 +424,9 @@ export class KeystoreManager {
|
|
|
351
424
|
if (typeof account === 'string') {
|
|
352
425
|
if (account.startsWith('0x') && account.length === 66) {
|
|
353
426
|
// Private key
|
|
354
|
-
return new LocalSigner(Buffer32.fromString(account
|
|
427
|
+
return new LocalSigner(Buffer32.fromString(ethPrivateKeySchema.parse(account)));
|
|
355
428
|
} else {
|
|
356
|
-
|
|
357
|
-
if (!defaultRemoteSigner) {
|
|
358
|
-
throw new KeystoreError(`No remote signer configuration found for address ${account}`);
|
|
359
|
-
}
|
|
360
|
-
return new RemoteSigner(EthAddress.fromString(account), defaultRemoteSigner);
|
|
429
|
+
throw new Error(`Invalid private key`);
|
|
361
430
|
}
|
|
362
431
|
}
|
|
363
432
|
|
|
@@ -368,35 +437,35 @@ export class KeystoreManager {
|
|
|
368
437
|
}
|
|
369
438
|
|
|
370
439
|
// Remote signer account
|
|
371
|
-
const remoteSigner = account
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
440
|
+
const remoteSigner: EthRemoteSignerAccount = account;
|
|
441
|
+
|
|
442
|
+
if ('address' in remoteSigner) {
|
|
443
|
+
// Remote signer with config
|
|
444
|
+
const config = remoteSigner.remoteSignerUrl
|
|
445
|
+
? {
|
|
446
|
+
remoteSignerUrl: remoteSigner.remoteSignerUrl,
|
|
447
|
+
certPath: remoteSigner.certPath,
|
|
448
|
+
certPass: remoteSigner.certPass,
|
|
449
|
+
}
|
|
450
|
+
: defaultRemoteSigner;
|
|
451
|
+
if (!config) {
|
|
452
|
+
throw new KeystoreError(`No remote signer configuration found for address ${remoteSigner.address}`);
|
|
376
453
|
}
|
|
377
|
-
return new RemoteSigner(EthAddress.fromString(remoteSigner), defaultRemoteSigner);
|
|
378
|
-
}
|
|
379
454
|
|
|
380
|
-
|
|
381
|
-
const config = remoteSigner.remoteSignerUrl
|
|
382
|
-
? {
|
|
383
|
-
remoteSignerUrl: remoteSigner.remoteSignerUrl,
|
|
384
|
-
certPath: remoteSigner.certPath,
|
|
385
|
-
certPass: remoteSigner.certPass,
|
|
386
|
-
}
|
|
387
|
-
: defaultRemoteSigner;
|
|
388
|
-
|
|
389
|
-
if (!config) {
|
|
390
|
-
throw new KeystoreError(`No remote signer configuration found for address ${remoteSigner.address}`);
|
|
455
|
+
return new RemoteSigner(remoteSigner.address, config);
|
|
391
456
|
}
|
|
392
457
|
|
|
393
|
-
|
|
458
|
+
// Just an address - use default config
|
|
459
|
+
if (!defaultRemoteSigner) {
|
|
460
|
+
throw new KeystoreError(`No remote signer configuration found for address ${remoteSigner}`);
|
|
461
|
+
}
|
|
462
|
+
return new RemoteSigner(remoteSigner, defaultRemoteSigner);
|
|
394
463
|
}
|
|
395
464
|
|
|
396
465
|
/**
|
|
397
466
|
* Create signer from JSON V3 keystore file or directory
|
|
398
467
|
*/
|
|
399
|
-
private createSignerFromJsonV3(config:
|
|
468
|
+
private createSignerFromJsonV3(config: EncryptedKeyFileConfig): EthSigner[] {
|
|
400
469
|
try {
|
|
401
470
|
const stats = statSync(config.path);
|
|
402
471
|
|
|
@@ -476,7 +545,7 @@ export class KeystoreManager {
|
|
|
476
545
|
/**
|
|
477
546
|
* Create signers from mnemonic configuration using BIP44 derivation
|
|
478
547
|
*/
|
|
479
|
-
private createSignersFromMnemonic(config:
|
|
548
|
+
private createSignersFromMnemonic(config: MnemonicConfig): EthSigner[] {
|
|
480
549
|
const { mnemonic, addressIndex = 0, accountIndex = 0, addressCount = 1, accountCount = 1 } = config;
|
|
481
550
|
const signers: EthSigner[] = [];
|
|
482
551
|
|
|
@@ -531,25 +600,18 @@ export class KeystoreManager {
|
|
|
531
600
|
const validator = this.getValidator(validatorIndex);
|
|
532
601
|
|
|
533
602
|
// Helper to get address from an account configuration
|
|
534
|
-
const getAddressFromAccount = (account: EthAccount): EthAddress | EthAddress[] |
|
|
603
|
+
const getAddressFromAccount = (account: EthAccount): EthAddress | EthAddress[] | undefined => {
|
|
535
604
|
if (typeof account === 'string') {
|
|
536
605
|
if (account.startsWith('0x') && account.length === 66) {
|
|
537
606
|
// This is a private key - derive the address
|
|
538
607
|
try {
|
|
539
|
-
const signer = new LocalSigner(Buffer32.fromString(account
|
|
608
|
+
const signer = new LocalSigner(Buffer32.fromString(ethPrivateKeySchema.parse(account)));
|
|
540
609
|
return signer.address;
|
|
541
610
|
} catch {
|
|
542
|
-
return
|
|
543
|
-
}
|
|
544
|
-
} else if (account.startsWith('0x') && account.length === 42) {
|
|
545
|
-
// This is an address
|
|
546
|
-
try {
|
|
547
|
-
return EthAddress.fromString(account);
|
|
548
|
-
} catch {
|
|
549
|
-
return null;
|
|
611
|
+
return undefined;
|
|
550
612
|
}
|
|
551
613
|
}
|
|
552
|
-
return
|
|
614
|
+
return undefined;
|
|
553
615
|
}
|
|
554
616
|
|
|
555
617
|
// JSON V3 keystore
|
|
@@ -558,18 +620,16 @@ export class KeystoreManager {
|
|
|
558
620
|
const signers = this.createSignerFromJsonV3(account);
|
|
559
621
|
return signers.map(s => s.address);
|
|
560
622
|
} catch {
|
|
561
|
-
return
|
|
623
|
+
return undefined;
|
|
562
624
|
}
|
|
563
625
|
}
|
|
564
626
|
|
|
565
|
-
// Remote signer account
|
|
566
|
-
const remoteSigner = account
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
return EthAddress.fromString(address);
|
|
570
|
-
} catch {
|
|
571
|
-
return null;
|
|
627
|
+
// Remote signer account, either it is an address or the address is nested
|
|
628
|
+
const remoteSigner: EthRemoteSignerAccount = account;
|
|
629
|
+
if ('address' in remoteSigner) {
|
|
630
|
+
return remoteSigner.address;
|
|
572
631
|
}
|
|
632
|
+
return remoteSigner;
|
|
573
633
|
};
|
|
574
634
|
|
|
575
635
|
// Helper to check if account matches and get its remote signer config
|
|
@@ -588,13 +648,7 @@ export class KeystoreManager {
|
|
|
588
648
|
|
|
589
649
|
// Found a match - determine the config to return
|
|
590
650
|
if (typeof account === 'string') {
|
|
591
|
-
|
|
592
|
-
// Private key - local signer, no remote config
|
|
593
|
-
return undefined;
|
|
594
|
-
} else {
|
|
595
|
-
// Address only - use defaults
|
|
596
|
-
return validator.remoteSigner || this.keystore.remoteSigner;
|
|
597
|
-
}
|
|
651
|
+
return undefined;
|
|
598
652
|
}
|
|
599
653
|
|
|
600
654
|
// JSON V3 - local signer, no remote config
|
|
@@ -603,57 +657,75 @@ export class KeystoreManager {
|
|
|
603
657
|
}
|
|
604
658
|
|
|
605
659
|
// Remote signer account with potential override
|
|
606
|
-
const remoteSigner = account
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
660
|
+
const remoteSigner: EthRemoteSignerAccount = account;
|
|
661
|
+
|
|
662
|
+
if ('address' in remoteSigner) {
|
|
663
|
+
// Has inline config
|
|
664
|
+
if (remoteSigner.remoteSignerUrl) {
|
|
665
|
+
return {
|
|
666
|
+
remoteSignerUrl: remoteSigner.remoteSignerUrl,
|
|
667
|
+
certPath: remoteSigner.certPath,
|
|
668
|
+
certPass: remoteSigner.certPass,
|
|
669
|
+
};
|
|
670
|
+
} else {
|
|
671
|
+
// No URL specified, use defaults
|
|
672
|
+
return validator.remoteSigner || this.keystore.remoteSigner;
|
|
673
|
+
}
|
|
610
674
|
}
|
|
675
|
+
// Just an address, use defaults
|
|
676
|
+
return validator.remoteSigner || this.keystore.remoteSigner;
|
|
677
|
+
};
|
|
611
678
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
};
|
|
619
|
-
} else {
|
|
620
|
-
// No URL specified, use defaults
|
|
621
|
-
return validator.remoteSigner || this.keystore.remoteSigner;
|
|
679
|
+
// Normalize attester to EthAccounts and search
|
|
680
|
+
const normalized = this.extractEthAccountsFromAttester(validator.attester);
|
|
681
|
+
|
|
682
|
+
const findInEthAccounts = (accs: EthAccounts): EthRemoteSignerConfig | undefined => {
|
|
683
|
+
if (typeof accs === 'string') {
|
|
684
|
+
return checkAccount(accs);
|
|
622
685
|
}
|
|
686
|
+
if (Array.isArray(accs)) {
|
|
687
|
+
for (const a of accs as EthAccount[]) {
|
|
688
|
+
const res = checkAccount(a);
|
|
689
|
+
if (res !== undefined) {
|
|
690
|
+
return res;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
if (typeof accs === 'object' && accs !== null && 'mnemonic' in accs) {
|
|
696
|
+
// mnemonic-derived keys are local signers; no remote signer config
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
return checkAccount(accs as EthAccount);
|
|
623
700
|
};
|
|
624
701
|
|
|
625
|
-
|
|
626
|
-
|
|
702
|
+
return findInEthAccounts(normalized);
|
|
703
|
+
}
|
|
627
704
|
|
|
705
|
+
/** Extract ETH accounts from AttesterAccounts */
|
|
706
|
+
private extractEthAccountsFromAttester(attester: AttesterAccounts): EthAccounts {
|
|
628
707
|
if (typeof attester === 'string') {
|
|
629
|
-
|
|
630
|
-
return result === undefined ? undefined : result;
|
|
708
|
+
return attester;
|
|
631
709
|
}
|
|
632
|
-
|
|
633
710
|
if (Array.isArray(attester)) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
|
|
711
|
+
const out: EthAccount[] = [];
|
|
712
|
+
for (const item of attester) {
|
|
713
|
+
if (typeof item === 'string') {
|
|
714
|
+
out.push(item);
|
|
715
|
+
} else if ('eth' in (item as any)) {
|
|
716
|
+
out.push((item as any).eth as EthAccount);
|
|
717
|
+
} else if (!('mnemonic' in (item as any))) {
|
|
718
|
+
out.push(item as EthAccount);
|
|
638
719
|
}
|
|
639
720
|
}
|
|
640
|
-
return
|
|
721
|
+
return out;
|
|
641
722
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if ('mnemonic' in attester) {
|
|
645
|
-
try {
|
|
646
|
-
const signers = this.createSignersFromMnemonic(attester);
|
|
647
|
-
const matches = signers.some(s => s.address.equals(attesterAddress));
|
|
648
|
-
// Mnemonic-derived keys are local signers
|
|
649
|
-
return matches ? undefined : undefined;
|
|
650
|
-
} catch {
|
|
651
|
-
return undefined;
|
|
652
|
-
}
|
|
723
|
+
if ('mnemonic' in (attester as any)) {
|
|
724
|
+
return attester as any;
|
|
653
725
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return
|
|
726
|
+
if ('eth' in (attester as any)) {
|
|
727
|
+
return (attester as any).eth as EthAccount;
|
|
728
|
+
}
|
|
729
|
+
return attester as any;
|
|
658
730
|
}
|
|
659
731
|
}
|
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))
|
|
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);
|
|
@@ -220,8 +223,9 @@ export function mergeKeystores(keystores: KeyStore[]): KeyStore {
|
|
|
220
223
|
if (keystore.validators) {
|
|
221
224
|
for (const validator of keystore.validators) {
|
|
222
225
|
// Check for duplicate attester addresses
|
|
223
|
-
const attesterKeys =
|
|
224
|
-
for (
|
|
226
|
+
const attesterKeys = extractAttesterAddresses(validator.attester);
|
|
227
|
+
for (let key of attesterKeys) {
|
|
228
|
+
key = key.toLowerCase();
|
|
225
229
|
if (attesterAddresses.has(key)) {
|
|
226
230
|
throw new KeyStoreLoadError(
|
|
227
231
|
`Duplicate attester address ${key} found across keystore files`,
|
|
@@ -284,18 +288,43 @@ export function mergeKeystores(keystores: KeyStore[]): KeyStore {
|
|
|
284
288
|
* @param attester The attester configuration in any supported shape.
|
|
285
289
|
* @returns Array of string keys used to detect duplicates.
|
|
286
290
|
*/
|
|
287
|
-
function
|
|
291
|
+
function extractAttesterAddresses(attester: unknown): string[] {
|
|
292
|
+
// String forms (private key or other) - return as-is for coarse uniqueness
|
|
288
293
|
if (typeof attester === 'string') {
|
|
289
|
-
|
|
294
|
+
if (attester.length === 66) {
|
|
295
|
+
return [privateKeyToAddress(attester as Hex<32>)];
|
|
296
|
+
} else {
|
|
297
|
+
return [attester];
|
|
298
|
+
}
|
|
290
299
|
}
|
|
291
300
|
|
|
301
|
+
// Arrays of attester items
|
|
292
302
|
if (Array.isArray(attester)) {
|
|
293
|
-
|
|
303
|
+
const keys: string[] = [];
|
|
304
|
+
for (const item of attester) {
|
|
305
|
+
keys.push(...extractAttesterAddresses(item));
|
|
306
|
+
}
|
|
307
|
+
return keys;
|
|
294
308
|
}
|
|
295
309
|
|
|
296
|
-
if (attester && typeof attester === 'object'
|
|
297
|
-
|
|
310
|
+
if (attester && typeof attester === 'object') {
|
|
311
|
+
if (attester instanceof EthAddress) {
|
|
312
|
+
return [attester.toString()];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const obj = attester as Record<string, unknown>;
|
|
316
|
+
|
|
317
|
+
// New shape: { eth: EthAccount, bls?: BLSAccount }
|
|
318
|
+
if ('eth' in obj) {
|
|
319
|
+
return extractAttesterAddresses(obj.eth);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Remote signer account object shape: { address, remoteSignerUrl?, ... }
|
|
323
|
+
if ('address' in obj) {
|
|
324
|
+
return [String((obj as any).address)];
|
|
325
|
+
}
|
|
298
326
|
}
|
|
299
327
|
|
|
300
|
-
|
|
328
|
+
// mnemonic, encrypted file just disable early duplicates checking
|
|
329
|
+
return [];
|
|
301
330
|
}
|