@aztec/node-keystore 0.0.1-commit.24de95ac
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 +7 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +10 -0
- package/dest/index.d.ts +7 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +6 -0
- package/dest/keystore_manager.d.ts +139 -0
- package/dest/keystore_manager.d.ts.map +1 -0
- package/dest/keystore_manager.js +579 -0
- package/dest/loader.d.ts +62 -0
- package/dest/loader.d.ts.map +1 -0
- package/dest/loader.js +274 -0
- package/dest/schemas.d.ts +1404 -0
- package/dest/schemas.d.ts.map +1 -0
- package/dest/schemas.js +103 -0
- package/dest/signer.d.ts +87 -0
- package/dest/signer.d.ts.map +1 -0
- package/dest/signer.js +228 -0
- package/dest/types.d.ts +116 -0
- package/dest/types.d.ts.map +1 -0
- package/dest/types.js +7 -0
- package/package.json +90 -0
- package/src/config.ts +16 -0
- package/src/index.ts +6 -0
- package/src/keystore_manager.ts +731 -0
- package/src/loader.ts +321 -0
- package/src/schemas.ts +111 -0
- package/src/signer.ts +352 -0
- package/src/types.ts +129 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keystore Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages keystore configuration and delegates signing operations to appropriate signers.
|
|
5
|
+
*/
|
|
6
|
+
import type { EthSigner } from '@aztec/ethereum';
|
|
7
|
+
import { Buffer32 } from '@aztec/foundation/buffer';
|
|
8
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
9
|
+
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
10
|
+
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
11
|
+
|
|
12
|
+
import { Wallet } from '@ethersproject/wallet';
|
|
13
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
14
|
+
import { extname, join } from 'path';
|
|
15
|
+
import type { TypedDataDefinition } from 'viem';
|
|
16
|
+
import { mnemonicToAccount } from 'viem/accounts';
|
|
17
|
+
|
|
18
|
+
import { ethPrivateKeySchema } from './schemas.js';
|
|
19
|
+
import { LocalSigner, RemoteSigner } from './signer.js';
|
|
20
|
+
import type {
|
|
21
|
+
AttesterAccounts,
|
|
22
|
+
EncryptedKeyFileConfig,
|
|
23
|
+
EthAccount,
|
|
24
|
+
EthAccounts,
|
|
25
|
+
EthRemoteSignerAccount,
|
|
26
|
+
EthRemoteSignerConfig,
|
|
27
|
+
KeyStore,
|
|
28
|
+
MnemonicConfig,
|
|
29
|
+
ProverKeyStore,
|
|
30
|
+
ValidatorKeyStore as ValidatorKeystoreConfig,
|
|
31
|
+
} from './types.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when keystore operations fail
|
|
35
|
+
*/
|
|
36
|
+
export class KeystoreError extends Error {
|
|
37
|
+
constructor(
|
|
38
|
+
message: string,
|
|
39
|
+
public override cause?: Error,
|
|
40
|
+
) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'KeystoreError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Keystore Manager - coordinates signing operations based on keystore configuration
|
|
48
|
+
*/
|
|
49
|
+
export class KeystoreManager {
|
|
50
|
+
private readonly keystore: KeyStore;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a keystore manager from a parsed configuration.
|
|
54
|
+
* Performs a lightweight duplicate-attester check without decrypting JSON V3 or deriving mnemonics.
|
|
55
|
+
* @param keystore Parsed keystore configuration
|
|
56
|
+
*/
|
|
57
|
+
constructor(keystore: KeyStore) {
|
|
58
|
+
this.keystore = keystore;
|
|
59
|
+
this.validateUniqueAttesterAddresses();
|
|
60
|
+
}
|
|
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
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validates that attester addresses are unique across all validators
|
|
140
|
+
* Only checks simple private key attesters, not JSON-V3 or mnemonic attesters,
|
|
141
|
+
* these are validated when decrypting the JSON-V3 keystore files
|
|
142
|
+
* @throws KeystoreError if duplicate attester addresses are found
|
|
143
|
+
*/
|
|
144
|
+
private validateUniqueAttesterAddresses(): void {
|
|
145
|
+
const seenAddresses = new Set<string>();
|
|
146
|
+
const validatorCount = this.getValidatorCount();
|
|
147
|
+
for (let validatorIndex = 0; validatorIndex < validatorCount; validatorIndex++) {
|
|
148
|
+
const validator = this.getValidator(validatorIndex);
|
|
149
|
+
const addresses = this.extractAddressesWithoutSensitiveOperations(validator.attester);
|
|
150
|
+
for (const addr of addresses) {
|
|
151
|
+
const address = addr.toString().toLowerCase();
|
|
152
|
+
if (seenAddresses.has(address)) {
|
|
153
|
+
throw new KeystoreError(
|
|
154
|
+
`Duplicate attester address found: ${addr.toString()}. An attester address may only appear once across all configuration blocks.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
seenAddresses.add(address);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Best-effort address extraction that avoids decryption/derivation (no JSON-V3 or mnemonic processing).
|
|
164
|
+
* This is used at construction time to check for obvious duplicates without throwing for invalid inputs.
|
|
165
|
+
*/
|
|
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[] {
|
|
175
|
+
const results: EthAddress[] = [];
|
|
176
|
+
|
|
177
|
+
const handleAccount = (account: EthAccount): void => {
|
|
178
|
+
if (typeof account === 'string') {
|
|
179
|
+
if (account.startsWith('0x') && account.length === 66) {
|
|
180
|
+
try {
|
|
181
|
+
const signer = new LocalSigner(Buffer32.fromString(ethPrivateKeySchema.parse(account)));
|
|
182
|
+
results.push(signer.address);
|
|
183
|
+
} catch {
|
|
184
|
+
// ignore invalid private key at construction time
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if ('path' in account) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if ('mnemonic' in (account as any)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const remoteSigner: EthRemoteSignerAccount = account;
|
|
199
|
+
if ('address' in remoteSigner) {
|
|
200
|
+
results.push(remoteSigner.address);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
results.push(remoteSigner);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (Array.isArray(accounts)) {
|
|
207
|
+
for (const account of accounts) {
|
|
208
|
+
handleAccount(account as EthAccount);
|
|
209
|
+
}
|
|
210
|
+
return results;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof accounts === 'object' && accounts !== null && 'mnemonic' in (accounts as any)) {
|
|
214
|
+
return results;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
handleAccount(accounts as EthAccount);
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create signers for validator attester accounts
|
|
223
|
+
*/
|
|
224
|
+
createAttesterSigners(validatorIndex: number): EthSigner[] {
|
|
225
|
+
const validator = this.getValidator(validatorIndex);
|
|
226
|
+
const ethAccounts = this.extractEthAccountsFromAttester(validator.attester);
|
|
227
|
+
return this.createSignersFromEthAccounts(ethAccounts, validator.remoteSigner || this.keystore.remoteSigner);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create signers for validator publisher accounts (falls back to attester if not specified)
|
|
232
|
+
*/
|
|
233
|
+
createPublisherSigners(validatorIndex: number): EthSigner[] {
|
|
234
|
+
const validator = this.getValidator(validatorIndex);
|
|
235
|
+
|
|
236
|
+
if (validator.publisher) {
|
|
237
|
+
return this.createSignersFromEthAccounts(
|
|
238
|
+
validator.publisher,
|
|
239
|
+
validator.remoteSigner || this.keystore.remoteSigner,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Fall back to attester signers
|
|
244
|
+
return this.createAttesterSigners(validatorIndex);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
createAllValidatorPublisherSigners(): EthSigner[] {
|
|
248
|
+
const numValidators = this.getValidatorCount();
|
|
249
|
+
const allPublishers = [];
|
|
250
|
+
|
|
251
|
+
for (let i = 0; i < numValidators; i++) {
|
|
252
|
+
allPublishers.push(...this.createPublisherSigners(i));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return allPublishers;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create signers for slasher accounts
|
|
260
|
+
*/
|
|
261
|
+
createSlasherSigners(): EthSigner[] {
|
|
262
|
+
if (!this.keystore.slasher) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return this.createSignersFromEthAccounts(this.keystore.slasher, this.keystore.remoteSigner);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create signers for prover accounts
|
|
271
|
+
*/
|
|
272
|
+
createProverSigners(): { id: EthAddress | undefined; signers: EthSigner[] } | undefined {
|
|
273
|
+
if (!this.keystore.prover) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Handle prover being a private key, JSON key store or remote signer with nested address
|
|
278
|
+
if (
|
|
279
|
+
typeof this.keystore.prover === 'string' ||
|
|
280
|
+
'path' in this.keystore.prover ||
|
|
281
|
+
'address' in this.keystore.prover
|
|
282
|
+
) {
|
|
283
|
+
const signers = this.createSignersFromEthAccounts(this.keystore.prover as EthAccount, this.keystore.remoteSigner);
|
|
284
|
+
return {
|
|
285
|
+
id: undefined,
|
|
286
|
+
signers,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
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
|
+
}
|
|
296
|
+
|
|
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
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get validator configuration by index
|
|
307
|
+
*/
|
|
308
|
+
getValidator(index: number): ValidatorKeystoreConfig {
|
|
309
|
+
if (!this.keystore.validators || index >= this.keystore.validators.length || index < 0) {
|
|
310
|
+
throw new KeystoreError(`Validator index ${index} out of bounds`);
|
|
311
|
+
}
|
|
312
|
+
return this.keystore.validators[index];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get validator count
|
|
317
|
+
*/
|
|
318
|
+
getValidatorCount(): number {
|
|
319
|
+
return this.keystore.validators?.length || 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get coinbase address for validator (falls back to the specific attester address)
|
|
324
|
+
*/
|
|
325
|
+
getCoinbaseAddress(validatorIndex: number, attesterAddress: EthAddress): EthAddress {
|
|
326
|
+
const validator = this.getValidator(validatorIndex);
|
|
327
|
+
|
|
328
|
+
if (validator.coinbase) {
|
|
329
|
+
return validator.coinbase;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Fall back to the specific attester address
|
|
333
|
+
return attesterAddress;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get fee recipient for validator
|
|
338
|
+
*/
|
|
339
|
+
getFeeRecipient(validatorIndex: number): AztecAddress {
|
|
340
|
+
const validator = this.getValidator(validatorIndex);
|
|
341
|
+
return validator.feeRecipient;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get the raw slasher configuration as provided in the keystore file.
|
|
346
|
+
* @returns The slasher accounts configuration or undefined if not set
|
|
347
|
+
*/
|
|
348
|
+
getSlasherAccounts(): EthAccounts | undefined {
|
|
349
|
+
return this.keystore.slasher;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get the raw prover configuration as provided in the keystore file.
|
|
354
|
+
* @returns The prover configuration or undefined if not set
|
|
355
|
+
*/
|
|
356
|
+
getProverConfig(): ProverKeyStore | undefined {
|
|
357
|
+
return this.keystore.prover;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Resolves attester accounts (including JSON V3 and mnemonic) and checks for duplicate addresses across validators.
|
|
362
|
+
* Throws if the same resolved address appears in more than one validator configuration.
|
|
363
|
+
*/
|
|
364
|
+
validateResolvedUniqueAttesterAddresses(): void {
|
|
365
|
+
const seenAddresses = new Set<string>();
|
|
366
|
+
const validatorCount = this.getValidatorCount();
|
|
367
|
+
for (let validatorIndex = 0; validatorIndex < validatorCount; validatorIndex++) {
|
|
368
|
+
const validator = this.getValidator(validatorIndex);
|
|
369
|
+
const signers = this.createSignersFromEthAccounts(
|
|
370
|
+
this.extractEthAccountsFromAttester(validator.attester),
|
|
371
|
+
validator.remoteSigner || this.keystore.remoteSigner,
|
|
372
|
+
);
|
|
373
|
+
for (const signer of signers) {
|
|
374
|
+
const address = signer.address.toString().toLowerCase();
|
|
375
|
+
if (seenAddresses.has(address)) {
|
|
376
|
+
throw new KeystoreError(
|
|
377
|
+
`Duplicate attester address found after resolving accounts: ${address}. An attester address may only appear once across all configuration blocks.`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
seenAddresses.add(address);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Create signers from EthAccounts configuration
|
|
387
|
+
*/
|
|
388
|
+
private createSignersFromEthAccounts(
|
|
389
|
+
accounts: EthAccounts,
|
|
390
|
+
defaultRemoteSigner?: EthRemoteSignerConfig,
|
|
391
|
+
): EthSigner[] {
|
|
392
|
+
if (typeof accounts === 'string') {
|
|
393
|
+
return [this.createSignerFromEthAccount(accounts, defaultRemoteSigner)];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (Array.isArray(accounts)) {
|
|
397
|
+
const signers: EthSigner[] = [];
|
|
398
|
+
for (const account of accounts) {
|
|
399
|
+
const accountSigners = this.createSignersFromEthAccounts(account, defaultRemoteSigner);
|
|
400
|
+
signers.push(...accountSigners);
|
|
401
|
+
}
|
|
402
|
+
return signers;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Mnemonic configuration
|
|
406
|
+
if ('mnemonic' in accounts) {
|
|
407
|
+
return this.createSignersFromMnemonic(accounts);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Single account object - handle JSON V3 directory case
|
|
411
|
+
if ('path' in accounts) {
|
|
412
|
+
const result = this.createSignerFromJsonV3(accounts);
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return [this.createSignerFromEthAccount(accounts, defaultRemoteSigner)];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Create a signer from a single EthAccount configuration
|
|
421
|
+
*/
|
|
422
|
+
private createSignerFromEthAccount(account: EthAccount, defaultRemoteSigner?: EthRemoteSignerConfig): EthSigner {
|
|
423
|
+
// Private key (hex string)
|
|
424
|
+
if (typeof account === 'string') {
|
|
425
|
+
if (account.startsWith('0x') && account.length === 66) {
|
|
426
|
+
// Private key
|
|
427
|
+
return new LocalSigner(Buffer32.fromString(ethPrivateKeySchema.parse(account)));
|
|
428
|
+
} else {
|
|
429
|
+
throw new Error(`Invalid private key`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// JSON V3 keystore
|
|
434
|
+
if ('path' in account) {
|
|
435
|
+
const result = this.createSignerFromJsonV3(account);
|
|
436
|
+
return result[0];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Remote signer account
|
|
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}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return new RemoteSigner(remoteSigner.address, config);
|
|
456
|
+
}
|
|
457
|
+
|
|
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);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Create signer from JSON V3 keystore file or directory
|
|
467
|
+
*/
|
|
468
|
+
private createSignerFromJsonV3(config: EncryptedKeyFileConfig): EthSigner[] {
|
|
469
|
+
try {
|
|
470
|
+
const stats = statSync(config.path);
|
|
471
|
+
|
|
472
|
+
if (stats.isDirectory()) {
|
|
473
|
+
// Handle directory - load all JSON files
|
|
474
|
+
const files = readdirSync(config.path);
|
|
475
|
+
const signers: EthSigner[] = [];
|
|
476
|
+
const seenAddresses = new Map<string, string>(); // address -> file name
|
|
477
|
+
|
|
478
|
+
for (const file of files) {
|
|
479
|
+
// Only process .json files
|
|
480
|
+
if (extname(file).toLowerCase() !== '.json') {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const filePath = join(config.path, file);
|
|
485
|
+
try {
|
|
486
|
+
const signer = this.createSignerFromSingleJsonV3File(filePath, config.password);
|
|
487
|
+
const addressString = signer.address.toString().toLowerCase();
|
|
488
|
+
const existingFile = seenAddresses.get(addressString);
|
|
489
|
+
if (existingFile) {
|
|
490
|
+
throw new KeystoreError(
|
|
491
|
+
`Duplicate JSON V3 keystore address ${addressString} found in directory ${config.path} (files: ${existingFile} and ${file}). Each keystore must have a unique address.`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
seenAddresses.set(addressString, file);
|
|
495
|
+
signers.push(signer);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
// Re-throw with file context
|
|
498
|
+
throw new KeystoreError(`Failed to load keystore file ${file}: ${error}`, error as Error);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (signers.length === 0) {
|
|
503
|
+
throw new KeystoreError(`No JSON keystore files found in directory ${config.path}`);
|
|
504
|
+
}
|
|
505
|
+
return signers;
|
|
506
|
+
} else {
|
|
507
|
+
// Single file
|
|
508
|
+
return [this.createSignerFromSingleJsonV3File(config.path, config.password)];
|
|
509
|
+
}
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (error instanceof KeystoreError) {
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
throw new KeystoreError(`Failed to access JSON V3 keystore ${config.path}: ${error}`, error as Error);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Create signer from a single JSON V3 keystore file
|
|
520
|
+
*/
|
|
521
|
+
private createSignerFromSingleJsonV3File(filePath: string, password?: string): EthSigner {
|
|
522
|
+
try {
|
|
523
|
+
// Read the keystore file
|
|
524
|
+
const keystoreJson = readFileSync(filePath, 'utf8');
|
|
525
|
+
|
|
526
|
+
// Get password - prompt for it if not provided
|
|
527
|
+
const resolvedPassword = password;
|
|
528
|
+
if (!resolvedPassword) {
|
|
529
|
+
throw new KeystoreError(`No password provided for keystore ${filePath}. Provide password in config.`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Use @ethersproject/wallet to decrypt the JSON V3 keystore synchronously
|
|
533
|
+
const ethersWallet = Wallet.fromEncryptedJsonSync(keystoreJson, resolvedPassword);
|
|
534
|
+
|
|
535
|
+
// Convert the private key to our format
|
|
536
|
+
const privateKey = Buffer32.fromString(ethersWallet.privateKey);
|
|
537
|
+
|
|
538
|
+
return new LocalSigner(privateKey);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
const err = error as Error;
|
|
541
|
+
throw new KeystoreError(`Failed to decrypt JSON V3 keystore ${filePath}: ${err.message}`, err);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Create signers from mnemonic configuration using BIP44 derivation
|
|
547
|
+
*/
|
|
548
|
+
private createSignersFromMnemonic(config: MnemonicConfig): EthSigner[] {
|
|
549
|
+
const { mnemonic, addressIndex = 0, accountIndex = 0, addressCount = 1, accountCount = 1 } = config;
|
|
550
|
+
const signers: EthSigner[] = [];
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
// Use viem's mnemonic derivation (imported at top of file)
|
|
554
|
+
|
|
555
|
+
// Normalize mnemonic by trimming whitespace
|
|
556
|
+
const normalizedMnemonic = mnemonic.trim();
|
|
557
|
+
|
|
558
|
+
for (let accIdx = accountIndex; accIdx < accountIndex + accountCount; accIdx++) {
|
|
559
|
+
for (let addrIdx = addressIndex; addrIdx < addressIndex + addressCount; addrIdx++) {
|
|
560
|
+
const viemAccount = mnemonicToAccount(normalizedMnemonic, {
|
|
561
|
+
accountIndex: accIdx,
|
|
562
|
+
addressIndex: addrIdx,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Extract the private key from the viem account
|
|
566
|
+
const privateKeyBytes = viemAccount.getHdKey().privateKey!;
|
|
567
|
+
const privateKey = Buffer32.fromBuffer(Buffer.from(privateKeyBytes));
|
|
568
|
+
signers.push(new LocalSigner(privateKey));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return signers;
|
|
573
|
+
} catch (error) {
|
|
574
|
+
throw new KeystoreError(`Failed to derive accounts from mnemonic: ${error}`, error as Error);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Sign message with a specific signer
|
|
580
|
+
*/
|
|
581
|
+
async signMessage(signer: EthSigner, message: Buffer32): Promise<Signature> {
|
|
582
|
+
return await signer.signMessage(message);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Sign typed data with a specific signer
|
|
587
|
+
*/
|
|
588
|
+
async signTypedData(signer: EthSigner, typedData: TypedDataDefinition): Promise<Signature> {
|
|
589
|
+
return await signer.signTypedData(typedData);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Get the effective remote signer configuration for a specific attester address
|
|
594
|
+
* Precedence: account-level override > validator-level config > file-level default
|
|
595
|
+
*/
|
|
596
|
+
getEffectiveRemoteSignerConfig(
|
|
597
|
+
validatorIndex: number,
|
|
598
|
+
attesterAddress: EthAddress,
|
|
599
|
+
): EthRemoteSignerConfig | undefined {
|
|
600
|
+
const validator = this.getValidator(validatorIndex);
|
|
601
|
+
|
|
602
|
+
// Helper to get address from an account configuration
|
|
603
|
+
const getAddressFromAccount = (account: EthAccount): EthAddress | EthAddress[] | undefined => {
|
|
604
|
+
if (typeof account === 'string') {
|
|
605
|
+
if (account.startsWith('0x') && account.length === 66) {
|
|
606
|
+
// This is a private key - derive the address
|
|
607
|
+
try {
|
|
608
|
+
const signer = new LocalSigner(Buffer32.fromString(ethPrivateKeySchema.parse(account)));
|
|
609
|
+
return signer.address;
|
|
610
|
+
} catch {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// JSON V3 keystore
|
|
618
|
+
if ('path' in account) {
|
|
619
|
+
try {
|
|
620
|
+
const signers = this.createSignerFromJsonV3(account);
|
|
621
|
+
return signers.map(s => s.address);
|
|
622
|
+
} catch {
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
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;
|
|
631
|
+
}
|
|
632
|
+
return remoteSigner;
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// Helper to check if account matches and get its remote signer config
|
|
636
|
+
const checkAccount = (account: EthAccount): EthRemoteSignerConfig | undefined => {
|
|
637
|
+
const addresses = getAddressFromAccount(account);
|
|
638
|
+
if (!addresses) {
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const addressArray = Array.isArray(addresses) ? addresses : [addresses];
|
|
643
|
+
const matches = addressArray.some(addr => addr.equals(attesterAddress));
|
|
644
|
+
|
|
645
|
+
if (!matches) {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Found a match - determine the config to return
|
|
650
|
+
if (typeof account === 'string') {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// JSON V3 - local signer, no remote config
|
|
655
|
+
if ('path' in account) {
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Remote signer account with potential override
|
|
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
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Just an address, use defaults
|
|
676
|
+
return validator.remoteSigner || this.keystore.remoteSigner;
|
|
677
|
+
};
|
|
678
|
+
|
|
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);
|
|
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);
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
return findInEthAccounts(normalized);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Extract ETH accounts from AttesterAccounts */
|
|
706
|
+
private extractEthAccountsFromAttester(attester: AttesterAccounts): EthAccounts {
|
|
707
|
+
if (typeof attester === 'string') {
|
|
708
|
+
return attester;
|
|
709
|
+
}
|
|
710
|
+
if (Array.isArray(attester)) {
|
|
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);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return out;
|
|
722
|
+
}
|
|
723
|
+
if ('mnemonic' in (attester as any)) {
|
|
724
|
+
return attester as any;
|
|
725
|
+
}
|
|
726
|
+
if ('eth' in (attester as any)) {
|
|
727
|
+
return (attester as any).eth as EthAccount;
|
|
728
|
+
}
|
|
729
|
+
return attester as any;
|
|
730
|
+
}
|
|
731
|
+
}
|