@chainsafe/lodestar 1.35.0-dev.e18102ed8c → 1.35.0-dev.f45a2be721

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.
Files changed (233) hide show
  1. package/.git-data.json +1 -1
  2. package/bin/lodestar.js +3 -0
  3. package/bin/lodestar.ts +3 -0
  4. package/lib/applyPreset.d.ts.map +1 -0
  5. package/lib/applyPreset.js +1 -1
  6. package/lib/applyPreset.js.map +1 -1
  7. package/lib/cli.d.ts.map +1 -0
  8. package/lib/cmds/beacon/handler.d.ts +1 -1
  9. package/lib/cmds/beacon/handler.d.ts.map +1 -0
  10. package/lib/cmds/beacon/handler.js +1 -1
  11. package/lib/cmds/beacon/handler.js.map +1 -1
  12. package/lib/cmds/beacon/index.d.ts.map +1 -0
  13. package/lib/cmds/beacon/initBeaconState.d.ts.map +1 -0
  14. package/lib/cmds/beacon/initPeerIdAndEnr.d.ts.map +1 -0
  15. package/lib/cmds/beacon/options.d.ts.map +1 -0
  16. package/lib/cmds/beacon/paths.d.ts.map +1 -0
  17. package/lib/cmds/bootnode/handler.d.ts.map +1 -0
  18. package/lib/cmds/bootnode/index.d.ts.map +1 -0
  19. package/lib/cmds/bootnode/options.d.ts.map +1 -0
  20. package/lib/cmds/dev/files.d.ts.map +1 -0
  21. package/lib/cmds/dev/handler.d.ts.map +1 -0
  22. package/lib/cmds/dev/index.d.ts.map +1 -0
  23. package/lib/cmds/dev/options.d.ts.map +1 -0
  24. package/lib/cmds/index.d.ts.map +1 -0
  25. package/lib/cmds/lightclient/handler.d.ts.map +1 -0
  26. package/lib/cmds/lightclient/index.d.ts.map +1 -0
  27. package/lib/cmds/lightclient/options.d.ts.map +1 -0
  28. package/lib/cmds/validator/blsToExecutionChange.d.ts.map +1 -0
  29. package/lib/cmds/validator/handler.d.ts.map +1 -0
  30. package/lib/cmds/validator/handler.js +1 -1
  31. package/lib/cmds/validator/handler.js.map +1 -1
  32. package/lib/cmds/validator/import.d.ts.map +1 -0
  33. package/lib/cmds/validator/index.d.ts.map +1 -0
  34. package/lib/cmds/validator/keymanager/decryptKeystoreDefinitions.d.ts.map +1 -0
  35. package/lib/cmds/validator/keymanager/decryptKeystores/index.d.ts.map +1 -0
  36. package/lib/cmds/validator/keymanager/decryptKeystores/poolSize.d.ts.map +1 -0
  37. package/lib/cmds/validator/keymanager/decryptKeystores/threadPool.d.ts.map +1 -0
  38. package/lib/cmds/validator/keymanager/decryptKeystores/types.d.ts.map +1 -0
  39. package/lib/cmds/validator/keymanager/decryptKeystores/worker.d.ts.map +1 -0
  40. package/lib/cmds/validator/keymanager/impl.d.ts.map +1 -0
  41. package/lib/cmds/validator/keymanager/interface.d.ts.map +1 -0
  42. package/lib/cmds/validator/keymanager/keystoreCache.d.ts.map +1 -0
  43. package/lib/cmds/validator/keymanager/persistedKeys.d.ts.map +1 -0
  44. package/lib/cmds/validator/keymanager/server.d.ts.map +1 -0
  45. package/lib/cmds/validator/list.d.ts.map +1 -0
  46. package/lib/cmds/validator/options.d.ts.map +1 -0
  47. package/lib/cmds/validator/options.js +5 -3
  48. package/lib/cmds/validator/options.js.map +1 -1
  49. package/lib/cmds/validator/paths.d.ts.map +1 -0
  50. package/lib/cmds/validator/signers/importExternalKeystores.d.ts.map +1 -0
  51. package/lib/cmds/validator/signers/index.d.ts.map +1 -0
  52. package/lib/cmds/validator/signers/logSigners.d.ts.map +1 -0
  53. package/lib/cmds/validator/slashingProtection/export.d.ts.map +1 -0
  54. package/lib/cmds/validator/slashingProtection/import.d.ts.map +1 -0
  55. package/lib/cmds/validator/slashingProtection/index.d.ts.map +1 -0
  56. package/lib/cmds/validator/slashingProtection/options.d.ts.map +1 -0
  57. package/lib/cmds/validator/slashingProtection/utils.d.ts.map +1 -0
  58. package/lib/cmds/validator/slashingProtection/utils.js +1 -1
  59. package/lib/cmds/validator/slashingProtection/utils.js.map +1 -1
  60. package/lib/cmds/validator/voluntaryExit.d.ts.map +1 -0
  61. package/lib/config/beaconNodeOptions.d.ts.map +1 -0
  62. package/lib/config/beaconParams.d.ts.map +1 -0
  63. package/lib/config/index.d.ts.map +1 -0
  64. package/lib/config/peerId.d.ts.map +1 -0
  65. package/lib/config/types.d.ts.map +1 -0
  66. package/lib/index.d.ts.map +1 -0
  67. package/lib/migrations/index.d.ts.map +1 -0
  68. package/lib/networks/chiado.d.ts.map +1 -0
  69. package/lib/networks/dev.d.ts.map +1 -0
  70. package/lib/networks/ephemery.d.ts.map +1 -0
  71. package/lib/networks/gnosis.d.ts.map +1 -0
  72. package/lib/networks/holesky.d.ts.map +1 -0
  73. package/lib/networks/hoodi.d.ts.map +1 -0
  74. package/lib/networks/index.d.ts.map +1 -0
  75. package/lib/networks/mainnet.d.ts.map +1 -0
  76. package/lib/networks/sepolia.d.ts.map +1 -0
  77. package/lib/options/beaconNodeOptions/api.d.ts +6 -0
  78. package/lib/options/beaconNodeOptions/api.d.ts.map +1 -0
  79. package/lib/options/beaconNodeOptions/api.js +14 -6
  80. package/lib/options/beaconNodeOptions/api.js.map +1 -1
  81. package/lib/options/beaconNodeOptions/builder.d.ts.map +1 -0
  82. package/lib/options/beaconNodeOptions/chain.d.ts.map +1 -0
  83. package/lib/options/beaconNodeOptions/eth1.d.ts.map +1 -0
  84. package/lib/options/beaconNodeOptions/execution.d.ts.map +1 -0
  85. package/lib/options/beaconNodeOptions/index.d.ts.map +1 -0
  86. package/lib/options/beaconNodeOptions/metrics.d.ts.map +1 -0
  87. package/lib/options/beaconNodeOptions/monitoring.d.ts.map +1 -0
  88. package/lib/options/beaconNodeOptions/network.d.ts.map +1 -0
  89. package/lib/options/beaconNodeOptions/sync.d.ts.map +1 -0
  90. package/lib/options/globalOptions.d.ts.map +1 -0
  91. package/lib/options/index.d.ts.map +1 -0
  92. package/lib/options/logOptions.d.ts.map +1 -0
  93. package/lib/options/paramsOptions.d.ts.map +1 -0
  94. package/lib/paths/global.d.ts.map +1 -0
  95. package/lib/paths/rootDir.d.ts.map +1 -0
  96. package/lib/util/errors.d.ts.map +1 -0
  97. package/lib/util/ethers.d.ts.map +1 -0
  98. package/lib/util/feeRecipient.d.ts.map +1 -0
  99. package/lib/util/file.d.ts.map +1 -0
  100. package/lib/util/file.js.map +1 -1
  101. package/lib/util/format.d.ts.map +1 -0
  102. package/lib/util/fs.d.ts.map +1 -0
  103. package/lib/util/gitData/gitDataPath.d.ts.map +1 -0
  104. package/lib/util/gitData/index.d.ts.map +1 -0
  105. package/lib/util/gitData/writeGitData.d.ts.map +1 -0
  106. package/lib/util/hasher_bun.d.ts +3 -0
  107. package/lib/util/hasher_bun.d.ts.map +1 -0
  108. package/lib/util/hasher_bun.js +118 -0
  109. package/lib/util/hasher_bun.js.map +1 -0
  110. package/lib/util/hasher_nodejs.d.ts +3 -0
  111. package/lib/util/hasher_nodejs.d.ts.map +1 -0
  112. package/lib/util/hasher_nodejs.js +3 -0
  113. package/lib/util/hasher_nodejs.js.map +1 -0
  114. package/lib/util/index.d.ts.map +1 -0
  115. package/lib/util/jwt.d.ts.map +1 -0
  116. package/lib/util/lockfile.d.ts.map +1 -0
  117. package/lib/util/logger.d.ts.map +1 -0
  118. package/lib/util/logger.js +1 -1
  119. package/lib/util/logger.js.map +1 -1
  120. package/lib/util/object.d.ts.map +1 -0
  121. package/lib/util/passphrase.d.ts.map +1 -0
  122. package/lib/util/process.d.ts.map +1 -0
  123. package/lib/util/progress.d.ts.map +1 -0
  124. package/lib/util/proposerConfig.d.ts.map +1 -0
  125. package/lib/util/pruneOldFilesInDir.d.ts.map +1 -0
  126. package/lib/util/sleep.d.ts.map +1 -0
  127. package/lib/util/stripOffNewlines.d.ts.map +1 -0
  128. package/lib/util/types.d.ts.map +1 -0
  129. package/lib/util/version.d.ts.map +1 -0
  130. package/package.json +32 -19
  131. package/src/applyPreset.ts +92 -0
  132. package/src/cli.ts +56 -0
  133. package/src/cmds/beacon/handler.ts +267 -0
  134. package/src/cmds/beacon/index.ts +18 -0
  135. package/src/cmds/beacon/initBeaconState.ts +275 -0
  136. package/src/cmds/beacon/initPeerIdAndEnr.ts +199 -0
  137. package/src/cmds/beacon/options.ts +214 -0
  138. package/src/cmds/beacon/paths.ts +62 -0
  139. package/src/cmds/bootnode/handler.ts +203 -0
  140. package/src/cmds/bootnode/index.ts +13 -0
  141. package/src/cmds/bootnode/options.ts +109 -0
  142. package/src/cmds/dev/files.ts +52 -0
  143. package/src/cmds/dev/handler.ts +86 -0
  144. package/src/cmds/dev/index.ts +18 -0
  145. package/src/cmds/dev/options.ts +110 -0
  146. package/src/cmds/index.ts +15 -0
  147. package/src/cmds/lightclient/handler.ts +36 -0
  148. package/src/cmds/lightclient/index.ts +18 -0
  149. package/src/cmds/lightclient/options.ts +21 -0
  150. package/src/cmds/validator/blsToExecutionChange.ts +91 -0
  151. package/src/cmds/validator/handler.ts +300 -0
  152. package/src/cmds/validator/import.ts +111 -0
  153. package/src/cmds/validator/index.ts +28 -0
  154. package/src/cmds/validator/keymanager/decryptKeystoreDefinitions.ts +189 -0
  155. package/src/cmds/validator/keymanager/decryptKeystores/index.ts +1 -0
  156. package/src/cmds/validator/keymanager/decryptKeystores/poolSize.ts +16 -0
  157. package/src/cmds/validator/keymanager/decryptKeystores/threadPool.ts +75 -0
  158. package/src/cmds/validator/keymanager/decryptKeystores/types.ts +12 -0
  159. package/src/cmds/validator/keymanager/decryptKeystores/worker.ts +24 -0
  160. package/src/cmds/validator/keymanager/impl.ts +425 -0
  161. package/src/cmds/validator/keymanager/interface.ts +35 -0
  162. package/src/cmds/validator/keymanager/keystoreCache.ts +91 -0
  163. package/src/cmds/validator/keymanager/persistedKeys.ts +268 -0
  164. package/src/cmds/validator/keymanager/server.ts +86 -0
  165. package/src/cmds/validator/list.ts +35 -0
  166. package/src/cmds/validator/options.ts +463 -0
  167. package/src/cmds/validator/paths.ts +95 -0
  168. package/src/cmds/validator/signers/importExternalKeystores.ts +69 -0
  169. package/src/cmds/validator/signers/index.ts +176 -0
  170. package/src/cmds/validator/signers/logSigners.ts +81 -0
  171. package/src/cmds/validator/slashingProtection/export.ts +110 -0
  172. package/src/cmds/validator/slashingProtection/import.ts +70 -0
  173. package/src/cmds/validator/slashingProtection/index.ts +12 -0
  174. package/src/cmds/validator/slashingProtection/options.ts +15 -0
  175. package/src/cmds/validator/slashingProtection/utils.ts +56 -0
  176. package/src/cmds/validator/voluntaryExit.ts +232 -0
  177. package/src/config/beaconNodeOptions.ts +68 -0
  178. package/src/config/beaconParams.ts +87 -0
  179. package/src/config/index.ts +3 -0
  180. package/src/config/peerId.ts +50 -0
  181. package/src/config/types.ts +3 -0
  182. package/src/index.ts +28 -0
  183. package/src/migrations/index.ts +0 -0
  184. package/src/networks/chiado.ts +20 -0
  185. package/src/networks/dev.ts +27 -0
  186. package/src/networks/ephemery.ts +9 -0
  187. package/src/networks/gnosis.ts +18 -0
  188. package/src/networks/holesky.ts +17 -0
  189. package/src/networks/hoodi.ts +16 -0
  190. package/src/networks/index.ts +236 -0
  191. package/src/networks/mainnet.ts +34 -0
  192. package/src/networks/sepolia.ts +17 -0
  193. package/src/options/beaconNodeOptions/api.ts +120 -0
  194. package/src/options/beaconNodeOptions/builder.ts +63 -0
  195. package/src/options/beaconNodeOptions/chain.ts +326 -0
  196. package/src/options/beaconNodeOptions/eth1.ts +95 -0
  197. package/src/options/beaconNodeOptions/execution.ts +92 -0
  198. package/src/options/beaconNodeOptions/index.ts +50 -0
  199. package/src/options/beaconNodeOptions/metrics.ts +39 -0
  200. package/src/options/beaconNodeOptions/monitoring.ts +61 -0
  201. package/src/options/beaconNodeOptions/network.ts +401 -0
  202. package/src/options/beaconNodeOptions/sync.ts +65 -0
  203. package/src/options/globalOptions.ts +72 -0
  204. package/src/options/index.ts +3 -0
  205. package/src/options/logOptions.ts +70 -0
  206. package/src/options/paramsOptions.ts +72 -0
  207. package/src/paths/global.ts +24 -0
  208. package/src/paths/rootDir.ts +11 -0
  209. package/src/util/errors.ts +20 -0
  210. package/src/util/ethers.ts +44 -0
  211. package/src/util/feeRecipient.ts +6 -0
  212. package/src/util/file.ts +167 -0
  213. package/src/util/format.ts +76 -0
  214. package/src/util/fs.ts +59 -0
  215. package/src/util/gitData/gitDataPath.ts +48 -0
  216. package/src/util/gitData/index.ts +70 -0
  217. package/src/util/gitData/writeGitData.ts +10 -0
  218. package/src/util/hasher_bun.ts +133 -0
  219. package/src/util/hasher_nodejs.ts +3 -0
  220. package/src/util/index.ts +17 -0
  221. package/src/util/jwt.ts +10 -0
  222. package/src/util/lockfile.ts +45 -0
  223. package/src/util/logger.ts +105 -0
  224. package/src/util/object.ts +15 -0
  225. package/src/util/passphrase.ts +25 -0
  226. package/src/util/process.ts +25 -0
  227. package/src/util/progress.ts +58 -0
  228. package/src/util/proposerConfig.ts +136 -0
  229. package/src/util/pruneOldFilesInDir.ts +27 -0
  230. package/src/util/sleep.ts +3 -0
  231. package/src/util/stripOffNewlines.ts +6 -0
  232. package/src/util/types.ts +8 -0
  233. package/src/util/version.ts +74 -0
@@ -0,0 +1,75 @@
1
+ import path from "node:path";
2
+ import {ModuleThread, Pool, QueuedTask, Worker, spawn} from "@chainsafe/threads";
3
+ import {maxPoolSize} from "./poolSize.js";
4
+ import {DecryptKeystoreArgs, DecryptKeystoreWorkerAPI} from "./types.js";
5
+
6
+ // Worker constructor consider the path relative to the current working directory
7
+ const workerDir =
8
+ process.env.NODE_ENV === "test" ? "../../../../../lib/cmds/validator/keymanager/decryptKeystores" : "./";
9
+
10
+ /**
11
+ * Thread pool to decrypt keystores
12
+ */
13
+ export class DecryptKeystoresThreadPool {
14
+ private pool: Pool<ModuleThread<DecryptKeystoreWorkerAPI>>;
15
+ private tasks: QueuedTask<ModuleThread<DecryptKeystoreWorkerAPI>, Uint8Array>[] = [];
16
+ private terminatePoolHandler: () => void;
17
+
18
+ constructor(
19
+ keystoreCount: number,
20
+ private readonly signal: AbortSignal
21
+ ) {
22
+ this.pool = Pool(
23
+ () =>
24
+ spawn<DecryptKeystoreWorkerAPI>(new Worker(path.join(workerDir, "worker.js")), {
25
+ // The number below is big enough to almost disable the timeout
26
+ // which helps during tests run on unpredictably slow hosts
27
+ timeout: 5 * 60 * 1000,
28
+ }),
29
+ {
30
+ // Adjust worker pool size based on keystore count
31
+ size: Math.min(keystoreCount, maxPoolSize),
32
+ // Decrypt keystores in sequence, increasing concurrency does not improve performance
33
+ concurrency: 1,
34
+ }
35
+ );
36
+ // Terminate worker threads when process receives exit signal
37
+ this.terminatePoolHandler = () => {
38
+ void this.pool.terminate(true);
39
+ };
40
+ signal.addEventListener("abort", this.terminatePoolHandler, {once: true});
41
+ }
42
+
43
+ /**
44
+ * Add keystore to the task queue to be decrypted
45
+ */
46
+ queue(
47
+ args: DecryptKeystoreArgs,
48
+ onDecrypted: (secretKeyBytes: Uint8Array) => void,
49
+ onError: (e: Error) => void
50
+ ): void {
51
+ const task = this.pool.queue((thread) => thread.decryptKeystore(args));
52
+ this.tasks.push(task);
53
+ task.then(onDecrypted).catch(onError);
54
+ }
55
+
56
+ /**
57
+ * Resolves once all queued tasks are completed and terminates worker threads.
58
+ * Errors during executing can be captured in `onError` handler for each task.
59
+ */
60
+ async completed(): Promise<void> {
61
+ await this.pool.settled(true);
62
+ await this.pool.terminate();
63
+ this.signal.removeEventListener("abort", this.terminatePoolHandler);
64
+ }
65
+
66
+ /**
67
+ * Cancel all pending tasks
68
+ */
69
+ cancel(): void {
70
+ for (const task of this.tasks) {
71
+ task.cancel();
72
+ }
73
+ this.tasks = [];
74
+ }
75
+ }
@@ -0,0 +1,12 @@
1
+ import {KeystoreStr} from "@lodestar/api/keymanager";
2
+ import {LocalKeystoreDefinition} from "../interface.js";
3
+
4
+ export type DecryptKeystoreWorkerAPI = {
5
+ decryptKeystore(args: DecryptKeystoreArgs): Promise<Uint8Array>;
6
+ };
7
+
8
+ export type DecryptKeystoreArgs = LocalKeystoreDefinition | {keystoreStr: KeystoreStr; password: string};
9
+
10
+ export function isLocalKeystoreDefinition(args: DecryptKeystoreArgs): args is LocalKeystoreDefinition {
11
+ return (args as LocalKeystoreDefinition).keystorePath !== undefined;
12
+ }
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs";
2
+ import {Keystore} from "@chainsafe/bls-keystore";
3
+ import {Transfer, TransferDescriptor} from "@chainsafe/threads";
4
+ import {expose} from "@chainsafe/threads/worker";
5
+ import {DecryptKeystoreArgs, DecryptKeystoreWorkerAPI, isLocalKeystoreDefinition} from "./types.js";
6
+
7
+ /**
8
+ * Decrypt a single keystore, returning the secret key as a Uint8Array
9
+ *
10
+ * NOTE: This is a memory (and cpu) -intensive process, since decrypting the keystore involves running a key derivation function (either pbkdf2 or scrypt)
11
+ */
12
+ export async function decryptKeystore(args: DecryptKeystoreArgs): Promise<TransferDescriptor<Uint8Array>> {
13
+ const keystore = Keystore.parse(
14
+ isLocalKeystoreDefinition(args) ? fs.readFileSync(args.keystorePath, "utf8") : args.keystoreStr
15
+ );
16
+
17
+ // Memory-hogging function
18
+ const secret = await keystore.decrypt(args.password);
19
+ // Transfer the underlying ArrayBuffer back to the main thread: https://threads.js.org/usage-advanced#transferable-objects
20
+ // This small performance gain may help in cases where this is run for many keystores
21
+ return Transfer(secret, [secret.buffer]);
22
+ }
23
+
24
+ expose({decryptKeystore} as unknown as DecryptKeystoreWorkerAPI);
@@ -0,0 +1,425 @@
1
+ import {Keystore} from "@chainsafe/bls-keystore";
2
+ import {SecretKey} from "@chainsafe/blst";
3
+ import {
4
+ BuilderBoostFactorData,
5
+ DeleteRemoteKeyStatus,
6
+ DeletionStatus,
7
+ FeeRecipientData,
8
+ GasLimitData,
9
+ GraffitiData,
10
+ ImportRemoteKeyStatus,
11
+ ImportStatus,
12
+ KeystoreStr,
13
+ ProposerConfigResponse,
14
+ PubkeyHex,
15
+ RemoteSignerDefinition,
16
+ ResponseStatus,
17
+ SignerDefinition,
18
+ SlashingProtectionData,
19
+ } from "@lodestar/api/keymanager";
20
+ import {KeymanagerApiMethods as Api} from "@lodestar/api/keymanager/server";
21
+ import {ApiError} from "@lodestar/api/server";
22
+ import {Epoch} from "@lodestar/types";
23
+ import {fromHex, isValidHttpUrl} from "@lodestar/utils";
24
+ import {Interchange, SignerType, Validator} from "@lodestar/validator";
25
+ import {getPubkeyHexFromKeystore, isValidatePubkeyHex} from "../../../util/format.js";
26
+ import {parseFeeRecipient} from "../../../util/index.js";
27
+ import {DecryptKeystoresThreadPool} from "./decryptKeystores/index.js";
28
+ import {IPersistedKeysBackend} from "./interface.js";
29
+
30
+ export class KeymanagerApi implements Api {
31
+ constructor(
32
+ private readonly validator: Validator,
33
+ private readonly persistedKeysBackend: IPersistedKeysBackend,
34
+ private readonly signal: AbortSignal,
35
+ private readonly proposerConfigWriteDisabled?: boolean
36
+ ) {}
37
+
38
+ private checkIfProposerWriteEnabled(): void {
39
+ if (this.proposerConfigWriteDisabled === true) {
40
+ throw Error("proposerSettingsFile option activated");
41
+ }
42
+ }
43
+
44
+ async listFeeRecipient({pubkey}: {pubkey: PubkeyHex}): ReturnType<Api["listFeeRecipient"]> {
45
+ this.assertValidKnownPubkey(pubkey);
46
+ return {data: {pubkey, ethaddress: this.validator.validatorStore.getFeeRecipient(pubkey)}};
47
+ }
48
+
49
+ async setFeeRecipient({pubkey, ethaddress}: FeeRecipientData): ReturnType<Api["setFeeRecipient"]> {
50
+ this.checkIfProposerWriteEnabled();
51
+ this.assertValidKnownPubkey(pubkey);
52
+ this.validator.validatorStore.setFeeRecipient(pubkey, parseFeeRecipient(ethaddress));
53
+ this.persistedKeysBackend.writeProposerConfig(pubkey, this.validator.validatorStore.getProposerConfig(pubkey));
54
+ return {status: 202};
55
+ }
56
+
57
+ async deleteFeeRecipient({pubkey}: {pubkey: PubkeyHex}): ReturnType<Api["deleteFeeRecipient"]> {
58
+ this.checkIfProposerWriteEnabled();
59
+ this.assertValidKnownPubkey(pubkey);
60
+ this.validator.validatorStore.deleteFeeRecipient(pubkey);
61
+ this.persistedKeysBackend.writeProposerConfig(pubkey, this.validator.validatorStore.getProposerConfig(pubkey));
62
+ return {status: 204};
63
+ }
64
+
65
+ async getGraffiti({pubkey}: {pubkey: PubkeyHex}): ReturnType<Api["getGraffiti"]> {
66
+ this.assertValidKnownPubkey(pubkey);
67
+ const graffiti = this.validator.validatorStore.getGraffiti(pubkey);
68
+ if (graffiti === undefined) {
69
+ throw new ApiError(404, `No graffiti for pubkey ${pubkey}`);
70
+ }
71
+ return {data: {pubkey, graffiti}};
72
+ }
73
+
74
+ async setGraffiti({pubkey, graffiti}: GraffitiData): ReturnType<Api["setGraffiti"]> {
75
+ this.checkIfProposerWriteEnabled();
76
+ this.assertValidKnownPubkey(pubkey);
77
+ this.validator.validatorStore.setGraffiti(pubkey, graffiti);
78
+ this.persistedKeysBackend.writeProposerConfig(pubkey, this.validator.validatorStore.getProposerConfig(pubkey));
79
+ return {status: 202};
80
+ }
81
+
82
+ async deleteGraffiti({pubkey}: {pubkey: PubkeyHex}): ReturnType<Api["deleteGraffiti"]> {
83
+ this.checkIfProposerWriteEnabled();
84
+ this.assertValidKnownPubkey(pubkey);
85
+ this.validator.validatorStore.deleteGraffiti(pubkey);
86
+ this.persistedKeysBackend.writeProposerConfig(pubkey, this.validator.validatorStore.getProposerConfig(pubkey));
87
+ return {status: 204};
88
+ }
89
+
90
+ async getGasLimit({pubkey}: {pubkey: PubkeyHex}): ReturnType<Api["getGasLimit"]> {
91
+ this.assertValidKnownPubkey(pubkey);
92
+ const gasLimit = this.validator.validatorStore.getGasLimit(pubkey);
93
+ return {data: {pubkey, gasLimit}};
94
+ }
95
+
96
+ async setGasLimit({pubkey, gasLimit}: GasLimitData): ReturnType<Api["setGasLimit"]> {
97
+ this.checkIfProposerWriteEnabled();
98
+ this.assertValidKnownPubkey(pubkey);
99
+ this.validator.validatorStore.setGasLimit(pubkey, gasLimit);
100
+ this.persistedKeysBackend.writeProposerConfig(pubkey, this.validator.validatorStore.getProposerConfig(pubkey));
101
+ return {status: 202};
102
+ }
103
+
104
+ async deleteGasLimit({pubkey}: {pubkey: PubkeyHex}): ReturnType<Api["deleteGasLimit"]> {
105
+ this.checkIfProposerWriteEnabled();
106
+ this.assertValidKnownPubkey(pubkey);
107
+ this.validator.validatorStore.deleteGasLimit(pubkey);
108
+ this.persistedKeysBackend.writeProposerConfig(pubkey, this.validator.validatorStore.getProposerConfig(pubkey));
109
+ return {status: 204};
110
+ }
111
+
112
+ async listKeys(): ReturnType<Api["listKeys"]> {
113
+ const localKeys = this.validator.validatorStore
114
+ .votingPubkeys()
115
+ .filter((pubkey) => this.validator.validatorStore.getSigner(pubkey)?.type === SignerType.Local);
116
+
117
+ return {
118
+ data: localKeys.map((pubkey) => ({
119
+ validatingPubkey: pubkey,
120
+ derivationPath: "",
121
+ readonly: false,
122
+ })),
123
+ };
124
+ }
125
+
126
+ async importKeystores({
127
+ keystores,
128
+ passwords,
129
+ slashingProtection,
130
+ }: {
131
+ keystores: KeystoreStr[];
132
+ passwords: string[];
133
+ slashingProtection?: SlashingProtectionData;
134
+ }): ReturnType<Api["importKeystores"]> {
135
+ if (slashingProtection) {
136
+ // The arguments to this function is passed in within the body of an HTTP request
137
+ // hence fastify will parse it into an object before this function is called.
138
+ // Even though the slashingProtection is typed as SlashingProtectionData,
139
+ // at runtime, when the handler for the request is selected, it would see slashingProtection
140
+ // as an object, hence trying to parse it using JSON.parse won't work. Instead, we cast straight to Interchange
141
+ const interchange = ensureJSON<Interchange>(slashingProtection);
142
+ await this.validator.importInterchange(interchange);
143
+ }
144
+
145
+ const statuses: {status: ImportStatus; message?: string}[] = [];
146
+ const decryptKeystores = new DecryptKeystoresThreadPool(keystores.length, this.signal);
147
+
148
+ for (let i = 0; i < keystores.length; i++) {
149
+ try {
150
+ const keystoreStr = keystores[i];
151
+ const password = passwords[i];
152
+ if (password === undefined) {
153
+ throw new ApiError(400, `No password for keystores[${i}]`);
154
+ }
155
+
156
+ const keystore = Keystore.parse(keystoreStr);
157
+ const pubkeyHex = getPubkeyHexFromKeystore(keystore);
158
+
159
+ // Check for duplicates and skip keystore before decrypting
160
+ if (this.validator.validatorStore.hasVotingPubkey(pubkeyHex)) {
161
+ statuses[i] = {status: ImportStatus.duplicate};
162
+ continue;
163
+ }
164
+
165
+ decryptKeystores.queue(
166
+ {keystoreStr, password},
167
+ async (secretKeyBytes: Uint8Array) => {
168
+ const secretKey = SecretKey.fromBytes(secretKeyBytes);
169
+
170
+ // Persist the key to disk for restarts, before adding to in-memory store
171
+ // If the keystore exist and has a lock it will throw
172
+ this.persistedKeysBackend.writeKeystore({
173
+ keystoreStr,
174
+ password,
175
+ // Lock immediately since it's gonna be used
176
+ lockBeforeWrite: true,
177
+ // Always write, even if it's already persisted for consistency.
178
+ // The in-memory validatorStore is the ground truth to decide duplicates
179
+ persistIfDuplicate: true,
180
+ });
181
+
182
+ // Add to in-memory store to start validating immediately
183
+ await this.validator.validatorStore.addSigner({type: SignerType.Local, secretKey});
184
+
185
+ statuses[i] = {status: ImportStatus.imported};
186
+ },
187
+ (e: Error) => {
188
+ statuses[i] = {status: ImportStatus.error, message: e.message};
189
+ }
190
+ );
191
+ } catch (e) {
192
+ statuses[i] = {status: ImportStatus.error, message: (e as Error).message};
193
+ }
194
+ }
195
+
196
+ await decryptKeystores.completed();
197
+
198
+ return {data: statuses};
199
+ }
200
+
201
+ async deleteKeys({pubkeys}: {pubkeys: PubkeyHex[]}): ReturnType<Api["deleteKeys"]> {
202
+ const deletedKey: boolean[] = [];
203
+ const statuses = new Array<{status: DeletionStatus; message?: string}>(pubkeys.length);
204
+
205
+ for (let i = 0; i < pubkeys.length; i++) {
206
+ try {
207
+ const pubkeyHex = pubkeys[i];
208
+
209
+ if (!isValidatePubkeyHex(pubkeyHex)) {
210
+ throw new ApiError(400, `Invalid pubkey ${pubkeyHex}`);
211
+ }
212
+
213
+ // Skip unknown keys or remote signers
214
+ const signer = this.validator.validatorStore.getSigner(pubkeyHex);
215
+ if (signer && signer.type === SignerType.Local) {
216
+ // Remove key from live local signer
217
+ deletedKey[i] = this.validator.validatorStore.removeSigner(pubkeyHex);
218
+
219
+ // Remove key from block duties
220
+ // Remove from attestation duties
221
+ // Remove from Sync committee duties
222
+ // Remove from indices
223
+ this.validator.removeDutiesForKey(pubkeyHex);
224
+ }
225
+
226
+ // Attempts to delete everything first, and returns status.
227
+ // This unlocks the keystore, so perform after deleting from in-memory store
228
+ const diskDeleteStatus = this.persistedKeysBackend.deleteKeystore(pubkeyHex);
229
+
230
+ if (diskDeleteStatus) {
231
+ // TODO: What if the diskDeleteStatus status is inconsistent?
232
+ deletedKey[i] = true;
233
+ }
234
+ } catch (e) {
235
+ statuses[i] = {status: DeletionStatus.error, message: (e as Error).message};
236
+ }
237
+ }
238
+
239
+ const pubkeysBytes = pubkeys.map((pubkeyHex) => fromHex(pubkeyHex));
240
+
241
+ const interchangeV5 = await this.validator.exportInterchange(pubkeysBytes, {
242
+ version: "5",
243
+ });
244
+
245
+ // After exporting slashing protection data in bulk, render the status
246
+ const pubkeysWithSlashingProtectionData = new Set(interchangeV5.data.map((data) => data.pubkey));
247
+ for (let i = 0; i < pubkeys.length; i++) {
248
+ if (statuses[i]?.status === DeletionStatus.error) {
249
+ continue;
250
+ }
251
+ const status = deletedKey[i]
252
+ ? DeletionStatus.deleted
253
+ : pubkeysWithSlashingProtectionData.has(pubkeys[i])
254
+ ? DeletionStatus.not_active
255
+ : DeletionStatus.not_found;
256
+ statuses[i] = {status};
257
+ }
258
+
259
+ return {
260
+ data: {
261
+ statuses,
262
+ slashingProtection: JSON.stringify(interchangeV5),
263
+ },
264
+ };
265
+ }
266
+
267
+ async listRemoteKeys(): ReturnType<Api["listRemoteKeys"]> {
268
+ const remoteKeys: SignerDefinition[] = [];
269
+
270
+ for (const pubkeyHex of this.validator.validatorStore.votingPubkeys()) {
271
+ const signer = this.validator.validatorStore.getSigner(pubkeyHex);
272
+ if (signer && signer.type === SignerType.Remote) {
273
+ remoteKeys.push({pubkey: signer.pubkey, url: signer.url, readonly: false});
274
+ }
275
+ }
276
+
277
+ return {
278
+ data: remoteKeys,
279
+ };
280
+ }
281
+
282
+ async importRemoteKeys({
283
+ remoteSigners,
284
+ }: {
285
+ remoteSigners: RemoteSignerDefinition[];
286
+ }): ReturnType<Api["importRemoteKeys"]> {
287
+ const importPromises = remoteSigners.map(async ({pubkey, url}): Promise<ResponseStatus<ImportRemoteKeyStatus>> => {
288
+ try {
289
+ if (!isValidatePubkeyHex(pubkey)) {
290
+ throw new ApiError(400, `Invalid pubkey ${pubkey}`);
291
+ }
292
+ if (!isValidHttpUrl(url)) {
293
+ throw new ApiError(400, `Invalid URL ${url}`);
294
+ }
295
+
296
+ // Check if key exists
297
+ if (this.validator.validatorStore.hasVotingPubkey(pubkey)) {
298
+ return {status: ImportRemoteKeyStatus.duplicate};
299
+ }
300
+
301
+ // Else try to add it
302
+
303
+ await this.validator.validatorStore.addSigner({type: SignerType.Remote, pubkey, url});
304
+
305
+ this.persistedKeysBackend.writeRemoteKey({
306
+ pubkey,
307
+ url,
308
+ // Always write, even if it's already persisted for consistency.
309
+ // The in-memory validatorStore is the ground truth to decide duplicates
310
+ persistIfDuplicate: true,
311
+ });
312
+
313
+ return {status: ImportRemoteKeyStatus.imported};
314
+ } catch (e) {
315
+ return {status: ImportRemoteKeyStatus.error, message: (e as Error).message};
316
+ }
317
+ });
318
+
319
+ return {
320
+ data: await Promise.all(importPromises),
321
+ };
322
+ }
323
+
324
+ async deleteRemoteKeys({pubkeys}: {pubkeys: PubkeyHex[]}): ReturnType<Api["deleteRemoteKeys"]> {
325
+ const results = pubkeys.map((pubkeyHex): ResponseStatus<DeleteRemoteKeyStatus> => {
326
+ try {
327
+ if (!isValidatePubkeyHex(pubkeyHex)) {
328
+ throw new ApiError(400, `Invalid pubkey ${pubkeyHex}`);
329
+ }
330
+
331
+ const signer = this.validator.validatorStore.getSigner(pubkeyHex);
332
+
333
+ // Remove key from live local signer
334
+ const deletedFromMemory =
335
+ signer && signer.type === SignerType.Remote ? this.validator.validatorStore.removeSigner(pubkeyHex) : false;
336
+
337
+ if (deletedFromMemory) {
338
+ // Remove duties if key was deleted from in-memory store
339
+ this.validator.removeDutiesForKey(pubkeyHex);
340
+ }
341
+
342
+ const deletedFromDisk = this.persistedKeysBackend.deleteRemoteKey(pubkeyHex);
343
+
344
+ return {
345
+ status:
346
+ deletedFromMemory || deletedFromDisk ? DeleteRemoteKeyStatus.deleted : DeleteRemoteKeyStatus.not_found,
347
+ };
348
+ } catch (e) {
349
+ return {status: DeleteRemoteKeyStatus.error, message: (e as Error).message};
350
+ }
351
+ });
352
+
353
+ return {
354
+ data: results,
355
+ };
356
+ }
357
+
358
+ async getBuilderBoostFactor({pubkey}: {pubkey: PubkeyHex}): ReturnType<Api["getBuilderBoostFactor"]> {
359
+ this.assertValidKnownPubkey(pubkey);
360
+ const builderBoostFactor = this.validator.validatorStore.getBuilderBoostFactor(pubkey);
361
+ return {data: {pubkey, builderBoostFactor}};
362
+ }
363
+
364
+ async setBuilderBoostFactor({
365
+ pubkey,
366
+ builderBoostFactor,
367
+ }: BuilderBoostFactorData): ReturnType<Api["setBuilderBoostFactor"]> {
368
+ this.checkIfProposerWriteEnabled();
369
+ this.assertValidKnownPubkey(pubkey);
370
+ this.validator.validatorStore.setBuilderBoostFactor(pubkey, builderBoostFactor);
371
+ this.persistedKeysBackend.writeProposerConfig(pubkey, this.validator.validatorStore.getProposerConfig(pubkey));
372
+ return {status: 202};
373
+ }
374
+
375
+ async deleteBuilderBoostFactor({pubkey}: {pubkey: PubkeyHex}): ReturnType<Api["deleteBuilderBoostFactor"]> {
376
+ this.checkIfProposerWriteEnabled();
377
+ this.validator.validatorStore.deleteBuilderBoostFactor(pubkey);
378
+ this.persistedKeysBackend.writeProposerConfig(pubkey, this.validator.validatorStore.getProposerConfig(pubkey));
379
+ return {status: 204};
380
+ }
381
+
382
+ async getProposerConfig({pubkey}: {pubkey: PubkeyHex}): ReturnType<Api["getProposerConfig"]> {
383
+ this.assertValidKnownPubkey(pubkey);
384
+
385
+ const config = this.validator.validatorStore.getProposerConfig(pubkey);
386
+
387
+ const data: ProposerConfigResponse = {
388
+ ...config,
389
+ builder: config?.builder
390
+ ? {
391
+ ...config.builder,
392
+ // Default JSON serialization can't handle BigInt
393
+ boostFactor: config.builder.boostFactor ? config.builder.boostFactor.toString() : undefined,
394
+ }
395
+ : undefined,
396
+ };
397
+
398
+ return {data};
399
+ }
400
+
401
+ async signVoluntaryExit({pubkey, epoch}: {pubkey: PubkeyHex; epoch?: Epoch}): ReturnType<Api["signVoluntaryExit"]> {
402
+ this.assertValidKnownPubkey(pubkey);
403
+ return {data: await this.validator.signVoluntaryExit(pubkey, epoch)};
404
+ }
405
+
406
+ private assertValidKnownPubkey(pubkey: PubkeyHex): void {
407
+ if (!isValidatePubkeyHex(pubkey)) {
408
+ throw new ApiError(400, `Invalid pubkey ${pubkey}`);
409
+ }
410
+
411
+ if (!this.validator.validatorStore.hasVotingPubkey(pubkey)) {
412
+ throw new ApiError(404, `Validator pubkey ${pubkey} not known`);
413
+ }
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Given a variable with JSON that maybe stringified or not, return parsed JSON
419
+ */
420
+ function ensureJSON<T>(strOrJson: string | T): T {
421
+ if (typeof strOrJson === "string") {
422
+ return JSON.parse(strOrJson) as T;
423
+ }
424
+ return strOrJson;
425
+ }
@@ -0,0 +1,35 @@
1
+ import {PubkeyHex, SignerDefinition} from "@lodestar/api/keymanager";
2
+ import {ProposerConfig} from "@lodestar/validator";
3
+
4
+ export type LocalKeystoreDefinition = {
5
+ keystorePath: string;
6
+ password: string;
7
+ };
8
+
9
+ export interface IPersistedKeysBackend {
10
+ readAllKeystores(): LocalKeystoreDefinition[];
11
+
12
+ /** Returns true some item is written to disk */
13
+ writeKeystore(args: {
14
+ keystoreStr: string;
15
+ password: string;
16
+ lockBeforeWrite: boolean;
17
+ persistIfDuplicate: boolean;
18
+ }): boolean;
19
+
20
+ /** Returns true some item is deleted from disk */
21
+ deleteKeystore(pubkey: PubkeyHex): boolean;
22
+
23
+ readAllRemoteKeys(): SignerDefinition[];
24
+
25
+ /** Returns true some item is written to disk */
26
+ writeRemoteKey(args: {pubkey: PubkeyHex; url: string; persistIfDuplicate: boolean}): boolean;
27
+
28
+ /** Returns true some item is deleted from disk */
29
+ deleteRemoteKey(pubkey: PubkeyHex): boolean;
30
+
31
+ writeProposerConfig(pubkey: PubkeyHex, proposerConfig: ProposerConfig | null): void;
32
+ deleteProposerConfig(pubkeyHex: PubkeyHex): void;
33
+ readProposerConfigs(): {[index: string]: ProposerConfig};
34
+ deleteProposerConfigs(): void;
35
+ }
@@ -0,0 +1,91 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {Keystore} from "@chainsafe/bls-keystore";
4
+ import {SecretKey} from "@chainsafe/blst";
5
+ import {fromHex, toHex, toPubkeyHex} from "@lodestar/utils";
6
+ import {SignerLocal, SignerType} from "@lodestar/validator";
7
+ import {writeFile600Perm} from "../../../util/file.js";
8
+ import {lockFilepath, unlockFilepath} from "../../../util/lockfile.js";
9
+ import {LocalKeystoreDefinition} from "./interface.js";
10
+
11
+ export async function loadKeystoreCache(
12
+ cacheFilepath: string,
13
+ keystoreDefinitions: LocalKeystoreDefinition[]
14
+ ): Promise<SignerLocal[]> {
15
+ const keystores: Keystore[] = [];
16
+ const passwords: string[] = [];
17
+ for (const {keystorePath, password} of keystoreDefinitions) {
18
+ keystores.push(Keystore.parse(fs.readFileSync(keystorePath, "utf8")));
19
+ passwords.push(password);
20
+ }
21
+
22
+ if (keystores.length !== passwords.length) {
23
+ throw new Error(
24
+ `Number of keystores and passwords must be equal. keystores=${keystores.length}, passwords=${passwords.length}`
25
+ );
26
+ }
27
+
28
+ if (!fs.existsSync(cacheFilepath)) {
29
+ throw new Error(`Cache file ${cacheFilepath} does not exists.`);
30
+ }
31
+
32
+ lockFilepath(cacheFilepath);
33
+
34
+ const password = passwords.join("");
35
+ // We can't use Keystore.parse as it validates the `encrypted message` to be only 32 bytes.
36
+ const keystore = new Keystore(JSON.parse(fs.readFileSync(cacheFilepath, "utf8")));
37
+ const secretKeyConcatenatedBytes = await keystore.decrypt(password);
38
+
39
+ const result: SignerLocal[] = [];
40
+ for (const [index, k] of keystores.entries()) {
41
+ const secretKeyBytes = Uint8Array.prototype.slice.call(secretKeyConcatenatedBytes, index * 32, (index + 1) * 32);
42
+ const secretKey = SecretKey.fromBytes(secretKeyBytes);
43
+ const publicKey = secretKey.toPublicKey().toBytes();
44
+
45
+ if (toPubkeyHex(publicKey) !== toPubkeyHex(fromHex(k.pubkey))) {
46
+ throw new Error(
47
+ `Keystore ${k.uuid} does not match the expected pubkey. expected=${toPubkeyHex(fromHex(k.pubkey))}, found=${toHex(
48
+ publicKey
49
+ )}`
50
+ );
51
+ }
52
+
53
+ result.push({
54
+ type: SignerType.Local,
55
+ secretKey,
56
+ });
57
+ }
58
+
59
+ unlockFilepath(cacheFilepath);
60
+
61
+ return result;
62
+ }
63
+
64
+ export async function writeKeystoreCache(
65
+ cacheFilepath: string,
66
+ signers: SignerLocal[],
67
+ passwords: string[]
68
+ ): Promise<void> {
69
+ if (signers.length !== passwords.length) {
70
+ throw new Error(
71
+ `Number of signers and passwords must be equal. signers=${signers.length}, passwords=${passwords.length}`
72
+ );
73
+ }
74
+ const secretKeys = signers.map((s) => s.secretKey.toBytes());
75
+ const publicKeys = signers.map((s) => s.secretKey.toPublicKey().toBytes());
76
+ const password = passwords.join("");
77
+ const secretKeyConcatenatedBytes = Buffer.concat(secretKeys);
78
+ const publicConcatenatedBytes = Buffer.concat(publicKeys);
79
+ const keystore = await Keystore.create(password, secretKeyConcatenatedBytes, publicConcatenatedBytes, cacheFilepath);
80
+ if (!fs.existsSync(path.dirname(cacheFilepath))) fs.mkdirSync(path.dirname(cacheFilepath), {recursive: true});
81
+ lockFilepath(cacheFilepath);
82
+ writeFile600Perm(cacheFilepath, keystore.stringify());
83
+ unlockFilepath(cacheFilepath);
84
+ }
85
+
86
+ export async function clearKeystoreCache(cacheFilepath: string): Promise<void> {
87
+ if (fs.existsSync(cacheFilepath)) {
88
+ unlockFilepath(cacheFilepath);
89
+ fs.unlinkSync(cacheFilepath);
90
+ }
91
+ }