@obolnetwork/obol-sdk 2.11.4 → 2.11.5-rc.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.
Files changed (144) hide show
  1. package/README.md +33 -0
  2. package/dist/browser/src/index.js +18280 -0
  3. package/dist/browser/src/index.js.map +1 -0
  4. package/dist/cjs/src/index.js +17470 -382
  5. package/dist/cjs/src/index.js.map +1 -0
  6. package/dist/esm/src/index.js +17437 -361
  7. package/dist/esm/src/index.js.map +1 -0
  8. package/dist/types/src/constants.d.ts +1 -2
  9. package/dist/types/src/eoa/eoaHelpers.d.ts +1 -1
  10. package/dist/types/src/exits/verificationHelpers.d.ts +0 -2
  11. package/dist/types/src/incentives/incentiveHelpers.d.ts +1 -1
  12. package/dist/types/src/splits/splitHelpers.d.ts +3 -3
  13. package/dist/types/src/utils.d.ts +2 -2
  14. package/dist/types/src/verification/common.d.ts +0 -2
  15. package/dist/types/src/verification/signature-validator.d.ts +4 -4
  16. package/dist/types/src/verification/v1.10.0.d.ts +1 -1
  17. package/dist/types/src/verification/v1.6.0.d.ts +1 -1
  18. package/dist/types/src/verification/v1.7.0.d.ts +1 -1
  19. package/dist/types/src/verification/v1.8.0.d.ts +1 -1
  20. package/dist/types/test/client/methods.spec.d.ts +17 -0
  21. package/dist/types/tsconfig.types.tsbuildinfo +1 -0
  22. package/dist/types/tsup.config.d.ts +2 -0
  23. package/package.json +96 -58
  24. package/dist/cjs/package.json +0 -139
  25. package/dist/cjs/src/abi/BatchDeposit.js +0 -120
  26. package/dist/cjs/src/abi/MerkleDistributorWithDeadline.js +0 -73
  27. package/dist/cjs/src/abi/Multicall3.js +0 -253
  28. package/dist/cjs/src/abi/OVM.js +0 -627
  29. package/dist/cjs/src/abi/OWR.js +0 -304
  30. package/dist/cjs/src/abi/SplitMain.js +0 -929
  31. package/dist/cjs/src/abi/splitV2FactoryAbi.js +0 -420
  32. package/dist/cjs/src/ajv.js +0 -136
  33. package/dist/cjs/src/base.js +0 -42
  34. package/dist/cjs/src/bytecodes.js +0 -19
  35. package/dist/cjs/src/constants.js +0 -272
  36. package/dist/cjs/src/eoa/eoa.js +0 -132
  37. package/dist/cjs/src/eoa/eoaHelpers.js +0 -82
  38. package/dist/cjs/src/errors.js +0 -11
  39. package/dist/cjs/src/exits/ethUtils.js +0 -57
  40. package/dist/cjs/src/exits/exit.js +0 -502
  41. package/dist/cjs/src/exits/verificationHelpers.js +0 -86
  42. package/dist/cjs/src/incentives/incentiveHelpers.js +0 -39
  43. package/dist/cjs/src/incentives/incentives.js +0 -119
  44. package/dist/cjs/src/schema.js +0 -352
  45. package/dist/cjs/src/services.js +0 -33
  46. package/dist/cjs/src/splits/splitHelpers.js +0 -571
  47. package/dist/cjs/src/splits/splits.js +0 -366
  48. package/dist/cjs/src/types.js +0 -29
  49. package/dist/cjs/src/utils.js +0 -77
  50. package/dist/cjs/src/verification/common.js +0 -355
  51. package/dist/cjs/src/verification/signature-validator.js +0 -73
  52. package/dist/cjs/src/verification/sszTypes.js +0 -69
  53. package/dist/cjs/src/verification/termsAndConditions.js +0 -40
  54. package/dist/cjs/src/verification/v1.10.0.js +0 -146
  55. package/dist/cjs/src/verification/v1.6.0.js +0 -149
  56. package/dist/cjs/src/verification/v1.7.0.js +0 -173
  57. package/dist/cjs/src/verification/v1.8.0.js +0 -196
  58. package/dist/cjs/test/client/ajv.spec.js +0 -320
  59. package/dist/cjs/test/client/methods.spec.js +0 -418
  60. package/dist/cjs/test/eoa/eoa.spec.js +0 -176
  61. package/dist/cjs/test/exit/ethUtils.spec.js +0 -83
  62. package/dist/cjs/test/exit/exit.spec.js +0 -399
  63. package/dist/cjs/test/exit/verificationHelpers.spec.js +0 -68
  64. package/dist/cjs/test/fixtures.js +0 -896
  65. package/dist/cjs/test/incentives/incentives.spec.js +0 -202
  66. package/dist/cjs/test/splits/splits.spec.js +0 -402
  67. package/dist/esm/package.json +0 -139
  68. package/dist/esm/src/abi/BatchDeposit.js +0 -117
  69. package/dist/esm/src/abi/MerkleDistributorWithDeadline.js +0 -70
  70. package/dist/esm/src/abi/Multicall3.js +0 -250
  71. package/dist/esm/src/abi/OVM.js +0 -624
  72. package/dist/esm/src/abi/OWR.js +0 -301
  73. package/dist/esm/src/abi/SplitMain.js +0 -926
  74. package/dist/esm/src/abi/splitV2FactoryAbi.js +0 -417
  75. package/dist/esm/src/ajv.js +0 -129
  76. package/dist/esm/src/base.js +0 -38
  77. package/dist/esm/src/bytecodes.js +0 -16
  78. package/dist/esm/src/constants.js +0 -238
  79. package/dist/esm/src/eoa/eoa.js +0 -128
  80. package/dist/esm/src/eoa/eoaHelpers.js +0 -77
  81. package/dist/esm/src/errors.js +0 -7
  82. package/dist/esm/src/exits/ethUtils.js +0 -52
  83. package/dist/esm/src/exits/exit.js +0 -475
  84. package/dist/esm/src/exits/verificationHelpers.js +0 -81
  85. package/dist/esm/src/incentives/incentiveHelpers.js +0 -34
  86. package/dist/esm/src/incentives/incentives.js +0 -115
  87. package/dist/esm/src/schema.js +0 -349
  88. package/dist/esm/src/services.js +0 -29
  89. package/dist/esm/src/splits/splitHelpers.js +0 -554
  90. package/dist/esm/src/splits/splits.js +0 -362
  91. package/dist/esm/src/types.js +0 -26
  92. package/dist/esm/src/utils.js +0 -68
  93. package/dist/esm/src/verification/common.js +0 -319
  94. package/dist/esm/src/verification/signature-validator.js +0 -64
  95. package/dist/esm/src/verification/sszTypes.js +0 -64
  96. package/dist/esm/src/verification/termsAndConditions.js +0 -33
  97. package/dist/esm/src/verification/v1.10.0.js +0 -140
  98. package/dist/esm/src/verification/v1.6.0.js +0 -142
  99. package/dist/esm/src/verification/v1.7.0.js +0 -166
  100. package/dist/esm/src/verification/v1.8.0.js +0 -189
  101. package/dist/esm/test/client/ajv.spec.js +0 -318
  102. package/dist/esm/test/client/methods.spec.js +0 -393
  103. package/dist/esm/test/eoa/eoa.spec.js +0 -174
  104. package/dist/esm/test/exit/ethUtils.spec.js +0 -81
  105. package/dist/esm/test/exit/exit.spec.js +0 -374
  106. package/dist/esm/test/exit/verificationHelpers.spec.js +0 -66
  107. package/dist/esm/test/fixtures.js +0 -893
  108. package/dist/esm/test/incentives/incentives.spec.js +0 -177
  109. package/dist/esm/test/splits/splits.spec.js +0 -400
  110. package/dist/types/src/verification/termsAndConditions.d.ts +0 -1
  111. package/src/abi/BatchDeposit.ts +0 -117
  112. package/src/abi/MerkleDistributorWithDeadline.ts +0 -70
  113. package/src/abi/Multicall3.ts +0 -250
  114. package/src/abi/OVM.ts +0 -624
  115. package/src/abi/OWR.ts +0 -302
  116. package/src/abi/SplitMain.ts +0 -927
  117. package/src/abi/splitV2FactoryAbi.ts +0 -417
  118. package/src/ajv.ts +0 -191
  119. package/src/base.ts +0 -54
  120. package/src/bytecodes.ts +0 -32
  121. package/src/constants.ts +0 -309
  122. package/src/eoa/eoa.ts +0 -153
  123. package/src/eoa/eoaHelpers.ts +0 -112
  124. package/src/errors.ts +0 -8
  125. package/src/exits/ethUtils.ts +0 -49
  126. package/src/exits/exit.ts +0 -661
  127. package/src/exits/verificationHelpers.ts +0 -110
  128. package/src/incentives/incentiveHelpers.ts +0 -55
  129. package/src/incentives/incentives.ts +0 -153
  130. package/src/index.ts +0 -614
  131. package/src/schema.ts +0 -376
  132. package/src/services.ts +0 -24
  133. package/src/splits/splitHelpers.ts +0 -1032
  134. package/src/splits/splits.ts +0 -504
  135. package/src/types.ts +0 -687
  136. package/src/utils.ts +0 -88
  137. package/src/verification/common.ts +0 -532
  138. package/src/verification/signature-validator.ts +0 -103
  139. package/src/verification/sszTypes.ts +0 -88
  140. package/src/verification/termsAndConditions.ts +0 -32
  141. package/src/verification/v1.10.0.ts +0 -245
  142. package/src/verification/v1.6.0.ts +0 -256
  143. package/src/verification/v1.7.0.ts +0 -309
  144. package/src/verification/v1.8.0.ts +0 -340
package/src/exits/exit.ts DELETED
@@ -1,661 +0,0 @@
1
- import { ENR } from '@chainsafe/discv5';
2
- import * as elliptic from 'elliptic';
3
- import { init, verify, aggregateSignatures } from '@chainsafe/bls';
4
- import {
5
- ByteVectorType,
6
- ContainerType,
7
- fromHexString,
8
- ListCompositeType,
9
- UintNumberType,
10
- } from '@chainsafe/ssz';
11
- import type {
12
- ProviderType,
13
- ExitClusterConfig,
14
- ExitValidationPayload,
15
- ExitValidationBlob,
16
- ExitValidationMessage,
17
- SignedExitValidationMessage,
18
- ExistingExitValidationBlobData,
19
- FullExitBlob,
20
- } from '../types';
21
- import { getCapellaFork, getGenesisValidatorsRoot } from './ethUtils';
22
- import { computeDomain, signingRoot } from './verificationHelpers';
23
-
24
- // Constants from obol-api/src/verification/exit.ts (assuming these might be needed or were in the original context)
25
- const DOMAIN_VOLUNTARY_EXIT = '0x04000000';
26
-
27
- // SSZ Type Definitions (adapted from obol-api/src/verification/exit.ts)
28
- const SSZExitMessageType = new ContainerType({
29
- epoch: new UintNumberType(8),
30
- validator_index: new UintNumberType(8),
31
- });
32
-
33
- const SSZPartialExitsPayloadType = new ContainerType({
34
- partial_exits: new ListCompositeType(
35
- new ContainerType({
36
- public_key: new ByteVectorType(48),
37
- signed_exit_message: new ContainerType({
38
- message: new ContainerType({
39
- epoch: new UintNumberType(8),
40
- validator_index: new UintNumberType(8),
41
- }),
42
- signature: new ByteVectorType(96),
43
- }),
44
- }),
45
- 65536,
46
- ),
47
- share_idx: new UintNumberType(8),
48
- });
49
-
50
- /**
51
- * Exit validation and verification class for Obol distributed validators.
52
- *
53
- * This class provides functionality to validate and verify voluntary exit signatures
54
- * for distributed validators in an Obol cluster. It handles both partial exit signatures
55
- * from individual operators and payload signatures that authorize exit operations.
56
- *
57
- * The class supports:
58
- * - Verification of BLS signatures for partial exit messages
59
- * - Verification of ECDSA signatures for exit payload authorization
60
- * - Validation of exit blobs against cluster configuration
61
- * - Duplicate detection and epoch validation
62
- *
63
- * @example
64
- * ```typescript
65
- * const exit = new Exit(1, provider); // Mainnet with provider
66
- *
67
- * // Verify a partial exit signature
68
- * const isValid = await exit.verifyPartialExitSignature(
69
- * publicShareKey,
70
- * signedExitMessage,
71
- * forkVersion,
72
- * genesisValidatorsRoot
73
- * );
74
- *
75
- * // Validate exit blobs for a cluster
76
- * const validBlobs = await exit.validateExitBlobs(
77
- * clusterConfig,
78
- * exitsPayload,
79
- * beaconNodeApiUrl,
80
- * existingBlobData
81
- * );
82
- * ```
83
- */
84
- export class Exit {
85
- public readonly chainId: number;
86
- public readonly provider: ProviderType | undefined | null;
87
-
88
- /**
89
- * Creates a new Exit instance for validator exit operations.
90
- *
91
- * @param chainId - The Ethereum chain ID (e.g., 1 for mainnet, 5 for goerli)
92
- * @param provider - Optional Ethereum provider for blockchain interactions
93
- *
94
- * @example
95
- * ```typescript
96
- * // For mainnet with a provider
97
- * const exit = new Exit(1, provider);
98
- *
99
- * // For goerli testnet without provider
100
- * const exit = new Exit(5, null);
101
- * ```
102
- */
103
- constructor(chainId: number, provider: ProviderType | undefined | null) {
104
- this.chainId = chainId;
105
- this.provider = provider;
106
- }
107
-
108
- /**
109
- * Safely parse a string integer to number, using BigInt to avoid precision loss
110
- * for large values beyond JavaScript's safe integer limit (2^53 - 1).
111
- * Throws an error if the value exceeds the safe range for a 64-bit unsigned integer.
112
- */
113
- private static safeParseInt(value: string): number {
114
- const bigIntValue = BigInt(value);
115
-
116
- // Check if value is within the range of a 64-bit unsigned integer
117
- const MAX_UINT64 = BigInt('0xFFFFFFFFFFFFFFFF');
118
- if (bigIntValue < 0 || bigIntValue > MAX_UINT64) {
119
- throw new Error(
120
- `Value ${value} is outside the valid range for a 64-bit unsigned integer`,
121
- );
122
- }
123
-
124
- // Convert to number - SSZ library should handle values even if they exceed JS safe integer limits
125
- return Number(bigIntValue);
126
- }
127
-
128
- private static computePartialExitMessageRoot(
129
- msg: ExitValidationMessage,
130
- ): Buffer {
131
- const sszValue = SSZExitMessageType.defaultValue();
132
-
133
- sszValue.epoch = Exit.safeParseInt(msg.epoch);
134
- sszValue.validator_index = Exit.safeParseInt(msg.validator_index);
135
- return Buffer.from(SSZExitMessageType.hashTreeRoot(sszValue).buffer);
136
- }
137
-
138
- private static computeExitPayloadRoot(exits: ExitValidationPayload): string {
139
- // Remove sorting since SSZ list ordering guarantees order
140
- // This eliminates the O(n log n) sort and improves performance
141
- const sszValue = SSZPartialExitsPayloadType.defaultValue();
142
- sszValue.partial_exits = exits.partial_exits.map(pe => ({
143
- public_key: fromHexString(pe.public_key),
144
- signed_exit_message: {
145
- message: {
146
- epoch: Exit.safeParseInt(pe.signed_exit_message.message.epoch),
147
- validator_index: Exit.safeParseInt(
148
- pe.signed_exit_message.message.validator_index,
149
- ),
150
- },
151
- signature: fromHexString(pe.signed_exit_message.signature),
152
- },
153
- }));
154
- sszValue.share_idx = exits.share_idx;
155
-
156
- return Buffer.from(
157
- SSZPartialExitsPayloadType.hashTreeRoot(sszValue).buffer,
158
- ).toString('hex');
159
- }
160
-
161
- /**
162
- * Verifies a partial exit signature from a distributed validator operator.
163
- *
164
- * This method validates that a partial exit signature was correctly signed by the
165
- * operator's share of the distributed validator's private key. It performs BLS
166
- * signature verification using the appropriate fork version and genesis validators root.
167
- *
168
- * @param publicShareKey - The operator's public share key (BLS public key, hex string with or without 0x prefix)
169
- * @param signedExitMessage - The signed exit message containing the exit details and signature
170
- * @param forkVersion - The Ethereum fork version (e.g., "0x00000000" for mainnet)
171
- * @param genesisValidatorsRootString - The genesis validators root for the network (hex string)
172
- *
173
- * @returns Promise resolving to true if the signature is valid, false otherwise
174
- *
175
- * @throws {Error} When unable to determine the Capella fork version for the given network
176
- * @throws {Error} When BLS library initialization or verification fails
177
- *
178
- * @example
179
- * ```typescript
180
- * const isValid = await exit.verifyPartialExitSignature(
181
- * "0x1234...abcd", // operator's public share key
182
- * {
183
- * message: { epoch: "12345", validator_index: "67890" },
184
- * signature: "0xabcd...1234"
185
- * },
186
- * "0x00000000", // mainnet fork version
187
- * "0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95"
188
- * );
189
- * ```
190
- */
191
- async verifyPartialExitSignature(
192
- publicShareKey: string,
193
- signedExitMessage: SignedExitValidationMessage,
194
- forkVersion: string,
195
- genesisValidatorsRootString: string,
196
- ): Promise<boolean> {
197
- await init('herumi');
198
-
199
- const capellaForkVersionString = await getCapellaFork(forkVersion);
200
- if (!capellaForkVersionString) {
201
- throw new Error(
202
- `Could not determine Capella fork version for base fork: ${forkVersion}`,
203
- );
204
- }
205
-
206
- const partialExitMessageBuffer = Exit.computePartialExitMessageRoot(
207
- signedExitMessage.message,
208
- );
209
-
210
- const exitDomain = computeDomain(
211
- fromHexString(DOMAIN_VOLUNTARY_EXIT),
212
- capellaForkVersionString,
213
- fromHexString(genesisValidatorsRootString),
214
- );
215
-
216
- const messageSigningRoot = signingRoot(
217
- exitDomain,
218
- partialExitMessageBuffer,
219
- );
220
-
221
- return verify(
222
- fromHexString(publicShareKey),
223
- messageSigningRoot,
224
- fromHexString(signedExitMessage.signature),
225
- );
226
- }
227
-
228
- /**
229
- * Verifies the exit payload signature using the operator's ENR.
230
- *
231
- * This method validates that an exit payload was signed by the correct operator
232
- * using ECDSA signature verification. The signature is verified against the
233
- * operator's public key extracted from their ENR (Ethereum Node Record).
234
- *
235
- * @param enrString - The operator's ENR string containing their identity and public key
236
- * @param exitsPayload - The exit validation payload containing partial exits and operator signature
237
- *
238
- * @returns Promise resolving to true if the payload signature is valid, false otherwise
239
- *
240
- * @throws {Error} When the ENR string is invalid or cannot be decoded
241
- * @throws {Error} When the signature format is invalid (must be 130 hex characters)
242
- * @throws {Error} When signature verification encounters an error
243
- *
244
- * @example
245
- * ```typescript
246
- * const isValid = await exit.verifyExitPayloadSignature(
247
- * "enr:-LK4QFo_n0dUm4PKejSOXf8JkSWq5EINV0XhG1zY00d...", // operator ENR
248
- * {
249
- * partial_exits: [exitBlob1, exitBlob2],
250
- * share_idx: 1,
251
- * signature: "0x1234...abcd" // ECDSA signature (130 hex chars)
252
- * }
253
- * );
254
- * ```
255
- */
256
- async verifyExitPayloadSignature(
257
- enrString: string,
258
- exitsPayload: ExitValidationPayload,
259
- ): Promise<boolean> {
260
- const partialExitsDtoHashRoot = Exit.computeExitPayloadRoot(exitsPayload);
261
- const ec = new elliptic.ec('secp256k1');
262
-
263
- let pubKeyHex;
264
- try {
265
- pubKeyHex = ENR.decodeTxt(enrString).publicKey.toString('hex');
266
- } catch (e: any) {
267
- throw new Error(
268
- `Invalid ENR string: ${enrString}. Error: ${e.message ?? String(e)}`,
269
- );
270
- }
271
-
272
- const sigHex = exitsPayload.signature.startsWith('0x')
273
- ? exitsPayload.signature.substring(2)
274
- : exitsPayload.signature;
275
-
276
- if (sigHex.length !== 130) {
277
- throw new Error(
278
- `Invalid signature length. Expected 130 hex chars (r + s), got ${sigHex.length}`,
279
- );
280
- }
281
-
282
- const r = sigHex.slice(0, 64);
283
- const s = sigHex.slice(64, 128);
284
-
285
- const enrSignature = { r, s };
286
-
287
- try {
288
- return ec
289
- .keyFromPublic(pubKeyHex, 'hex')
290
- .verify(
291
- partialExitsDtoHashRoot,
292
- enrSignature as elliptic.ec.SignatureOptions,
293
- );
294
- } catch (e: any) {
295
- throw new Error(
296
- `Signature verification failed: ${e.message ?? String(e)}`,
297
- );
298
- }
299
- }
300
-
301
- private async validateOperatorAndPayload(
302
- clusterConfig: ExitClusterConfig,
303
- exitsPayload: ExitValidationPayload,
304
- ): Promise<string> {
305
- const operatorIndex = exitsPayload.share_idx - 1;
306
- if (
307
- operatorIndex < 0 ||
308
- operatorIndex >= clusterConfig.definition.operators.length
309
- ) {
310
- throw new Error(
311
- `Invalid share_idx ${exitsPayload.share_idx} for ${clusterConfig.definition.operators.length} operators.`,
312
- );
313
- }
314
- const operatorEnr = clusterConfig.definition.operators[operatorIndex].enr;
315
-
316
- const isPayloadSignatureValid = await this.verifyExitPayloadSignature(
317
- operatorEnr,
318
- exitsPayload,
319
- );
320
- if (!isPayloadSignatureValid) {
321
- throw new Error('Incorrect payload signature for partial exits.');
322
- }
323
-
324
- return operatorEnr;
325
- }
326
-
327
- private async getNetworkParameters(
328
- forkVersion: string,
329
- beaconNodeApiUrl: string,
330
- ): Promise<{ genesisValidatorsRoot: string; capellaForkVersion: string }> {
331
- const genesisValidatorsRootString =
332
- await getGenesisValidatorsRoot(beaconNodeApiUrl);
333
- if (!genesisValidatorsRootString) {
334
- throw new Error('Could not retrieve genesis validators root.');
335
- }
336
-
337
- const capellaForkVersionString = await getCapellaFork(forkVersion);
338
- if (!capellaForkVersionString) {
339
- throw new Error(
340
- `Unsupported network: Could not determine Capella fork for ${forkVersion}`,
341
- );
342
- }
343
-
344
- return {
345
- genesisValidatorsRoot: genesisValidatorsRootString,
346
- capellaForkVersion: capellaForkVersionString,
347
- };
348
- }
349
-
350
- private findValidatorInCluster(
351
- clusterConfig: ExitClusterConfig,
352
- publicKey: string,
353
- operatorIndex: number,
354
- ): { validator: any; publicShare: string } {
355
- const validatorInCluster = clusterConfig.distributed_validators.find(
356
- dv =>
357
- (dv.distributed_public_key.startsWith('0x')
358
- ? dv.distributed_public_key
359
- : '0x' + dv.distributed_public_key
360
- ).toLowerCase() ===
361
- (publicKey.startsWith('0x')
362
- ? publicKey
363
- : '0x' + publicKey
364
- ).toLowerCase(),
365
- );
366
-
367
- if (!validatorInCluster) {
368
- throw new Error(
369
- `Public key ${publicKey} not found in the cluster's distributed validators.`,
370
- );
371
- }
372
-
373
- const publicShareForOperator =
374
- validatorInCluster.public_shares[operatorIndex];
375
- if (!publicShareForOperator) {
376
- throw new Error(
377
- `Public share for operator index ${operatorIndex} not found for validator ${publicKey}`,
378
- );
379
- }
380
-
381
- return {
382
- validator: validatorInCluster,
383
- publicShare: publicShareForOperator,
384
- };
385
- }
386
-
387
- private async validateExistingBlobData(
388
- exitBlob: ExitValidationBlob,
389
- existingBlob: ExistingExitValidationBlobData | null,
390
- operatorIndex: number,
391
- ): Promise<boolean> {
392
- if (!existingBlob) {
393
- return false;
394
- }
395
-
396
- // Check if existing blob data is for this public key
397
- const normalizeKey = (key: string): string =>
398
- (key.startsWith('0x') ? key : '0x' + key).toLowerCase();
399
-
400
- if (
401
- normalizeKey(existingBlob.public_key) !==
402
- normalizeKey(exitBlob.public_key)
403
- ) {
404
- return false; // Existing blob data is for a different validator
405
- }
406
-
407
- if (
408
- existingBlob.validator_index !==
409
- exitBlob.signed_exit_message.message.validator_index
410
- ) {
411
- throw new Error(
412
- `Validator index mismatch for already processed exit for public key ${exitBlob.public_key}. Expected ${existingBlob.validator_index}, got ${exitBlob.signed_exit_message.message.validator_index}.`,
413
- );
414
- }
415
-
416
- const currentEpoch = Exit.safeParseInt(
417
- exitBlob.signed_exit_message.message.epoch,
418
- );
419
- const existingEpoch = Exit.safeParseInt(existingBlob.epoch);
420
-
421
- if (currentEpoch < existingEpoch) {
422
- throw new Error(
423
- `New exit epoch ${currentEpoch} is not greater than existing exit epoch ${existingEpoch} for validator ${exitBlob.public_key}.`,
424
- );
425
- } else if (currentEpoch === existingEpoch) {
426
- const operatorShareIndexString = String(operatorIndex);
427
- if (
428
- existingBlob.shares_exit_data?.[0]?.[operatorShareIndexString]
429
- ?.partial_exit_signature &&
430
- existingBlob.shares_exit_data[0][operatorShareIndexString]
431
- .partial_exit_signature !== exitBlob.signed_exit_message.signature
432
- ) {
433
- throw new Error(
434
- `Signature mismatch for validator ${exitBlob.public_key}, operator index ${operatorIndex} at epoch ${currentEpoch}. Received different signature than existing.`,
435
- );
436
- }
437
- return true; // Already processed
438
- }
439
-
440
- return false;
441
- }
442
-
443
- private async processExitBlob(
444
- exitBlob: ExitValidationBlob,
445
- clusterConfig: ExitClusterConfig,
446
- operatorIndex: number,
447
- genesisValidatorsRoot: string,
448
- existingBlobData: ExistingExitValidationBlobData | null,
449
- ): Promise<ExitValidationBlob | null> {
450
- const { publicShare } = this.findValidatorInCluster(
451
- clusterConfig,
452
- exitBlob.public_key,
453
- operatorIndex,
454
- );
455
-
456
- const alreadyProcessed = await this.validateExistingBlobData(
457
- exitBlob,
458
- existingBlobData,
459
- operatorIndex,
460
- );
461
-
462
- if (alreadyProcessed) {
463
- return null;
464
- }
465
-
466
- const isPartialSignatureValid = await this.verifyPartialExitSignature(
467
- publicShare,
468
- exitBlob.signed_exit_message,
469
- clusterConfig.definition.fork_version,
470
- genesisValidatorsRoot,
471
- );
472
-
473
- if (!isPartialSignatureValid) {
474
- throw new Error(
475
- `Invalid partial exit signature for validator ${exitBlob.public_key} by operator index ${operatorIndex}.`,
476
- );
477
- }
478
-
479
- return exitBlob;
480
- }
481
-
482
- /**
483
- * Validates exit blobs against cluster configuration and existing data.
484
- *
485
- * This method performs comprehensive validation of exit blobs including:
486
- * - Operator authorization and payload signature verification
487
- * - Network parameter validation (genesis root, fork version)
488
- * - Public key validation against cluster configuration
489
- * - Partial signature verification for each exit blob
490
- * - Duplicate detection and epoch progression validation
491
- * - Signature consistency checks for existing exits
492
- *
493
- * @param clusterConfig - The cluster configuration containing operators and distributed validators
494
- * @param exitsPayload - The exit validation payload with partial exits and operator info
495
- * @param beaconNodeApiUrl - The beacon node API URL for network parameter retrieval
496
- * @param existingBlobData - Existing exit blob data for duplicate detection, or null if none exists
497
- *
498
- * @returns Promise resolving to an array of validated, non-duplicate exit blobs
499
- *
500
- * @throws {Error} When share_idx is invalid or out of bounds for the cluster operators
501
- * @throws {Error} When payload signature verification fails
502
- * @throws {Error} When network parameters cannot be retrieved or are invalid
503
- * @throws {Error} When a public key is not found in the cluster's distributed validators
504
- * @throws {Error} When a partial exit signature is invalid
505
- * @throws {Error} When exit epoch validation fails (new epoch not greater than existing)
506
- * @throws {Error} When validator index mismatches with existing data
507
- * @throws {Error} When signature mismatches for the same epoch and operator
508
- *
509
- * @example
510
- * ```typescript
511
- * const validExitBlobs = await exit.validateExitBlobs(
512
- * {
513
- * definition: {
514
- * operators: [{ enr: "enr:-LK4Q..." }],
515
- * fork_version: "0x00000000",
516
- * threshold: 1
517
- * },
518
- * distributed_validators: [{
519
- * distributed_public_key: "0x1234...abcd",
520
- * public_shares: ["0x5678...efgh"]
521
- * }]
522
- * },
523
- * {
524
- * partial_exits: [exitBlob],
525
- * share_idx: 1,
526
- * signature: "0x1234...abcd"
527
- * },
528
- * "http://localhost:5052",
529
- * existingBlobData // or null for new exits
530
- * );
531
- * ```
532
- */
533
- async validateExitBlobs(
534
- clusterConfig: ExitClusterConfig,
535
- exitsPayload: ExitValidationPayload,
536
- beaconNodeApiUrl: string,
537
- existingBlobData: ExistingExitValidationBlobData | null,
538
- ): Promise<ExitValidationBlob[]> {
539
- await this.validateOperatorAndPayload(clusterConfig, exitsPayload);
540
-
541
- const { genesisValidatorsRoot } = await this.getNetworkParameters(
542
- clusterConfig.definition.fork_version,
543
- beaconNodeApiUrl,
544
- );
545
-
546
- const operatorIndex = exitsPayload.share_idx - 1;
547
- const validNonDuplicateBlobs: ExitValidationBlob[] = [];
548
-
549
- for (const currentExitBlob of exitsPayload.partial_exits) {
550
- const processedBlob = await this.processExitBlob(
551
- currentExitBlob,
552
- clusterConfig,
553
- operatorIndex,
554
- genesisValidatorsRoot,
555
- existingBlobData,
556
- );
557
-
558
- if (processedBlob) {
559
- validNonDuplicateBlobs.push(processedBlob);
560
- }
561
- }
562
-
563
- return validNonDuplicateBlobs;
564
- }
565
-
566
- /**
567
- * Recombines exit blobs into a single exit blob.
568
- *
569
- * This method aggregates partial exit signatures from multiple operators into a single exit blob.
570
- * It ensures that the signatures are properly ordered and aggregated according to the operator indices.
571
- *
572
- * @param exitBlob - The existing exit blob data containing partial exit signatures
573
- *
574
- * @returns Promise resolving to a single exit blob with aggregated signatures
575
- *
576
- * @throws {Error} When no valid signatures are found for aggregation
577
- * @throws {Error} When signature length is invalid
578
- * @throws {Error} When signature parsing fails
579
- *
580
- * @example
581
- * ```typescript
582
- * const aggregatedExitBlob = await exit.recombineExitBlobs(existingBlobData);
583
- * ```
584
- */
585
- async recombineExitBlobs(
586
- exitBlob: ExistingExitValidationBlobData,
587
- ): Promise<FullExitBlob> {
588
- await init('herumi');
589
-
590
- // Map to store signatures by their share index (matching Go's map[int]tbls.Signature)
591
- const signaturesByIndex = new Map<number, Uint8Array>();
592
-
593
- // Extract signatures from shares_exit_data (equivalent to er.Signatures in Go)
594
- if (!exitBlob.shares_exit_data || exitBlob.shares_exit_data.length === 0) {
595
- throw new Error('No shares exit data available for aggregation');
596
- }
597
- const signaturesMap = exitBlob.shares_exit_data[0] || {};
598
- for (const [sigIdxStr, sigData] of Object.entries(signaturesMap)) {
599
- const sigStr = sigData.partial_exit_signature;
600
-
601
- if (!sigStr || sigStr.length === 0) {
602
- // ignore, the associated share index didn't push a partial signature yet
603
- continue;
604
- }
605
-
606
- if (sigStr.length < 2) {
607
- throw new Error(`Signature string has invalid size: ${sigStr.length}`);
608
- }
609
-
610
- // Remove 0x prefix and ensure it's 96 bytes (192 hex chars)
611
- const cleanSigStr = sigStr.startsWith('0x')
612
- ? sigStr.substring(2)
613
- : sigStr;
614
- if (cleanSigStr.length !== 192) {
615
- throw new Error(
616
- `Invalid signature length. Expected 192 hex chars (96 bytes), got ${cleanSigStr.length}`,
617
- );
618
- }
619
-
620
- try {
621
- const sigBytes = fromHexString(cleanSigStr);
622
- // Convert string index to number and add 1 (matching Go's sigIdx+1)
623
- const sigIdx = parseInt(sigIdxStr, 10);
624
- signaturesByIndex.set(sigIdx + 1, sigBytes);
625
- } catch (err) {
626
- throw new Error(`Invalid partial signature: ${String(err)}`);
627
- }
628
- }
629
-
630
- if (signaturesByIndex.size === 0) {
631
- throw new Error('No valid signatures found for aggregation');
632
- }
633
-
634
- // Sort by index and extract signatures in correct order
635
- const sortedIndices = Array.from(signaturesByIndex.keys()).sort(
636
- (a, b) => a - b,
637
- );
638
- const rawSignatures = sortedIndices.map(idx => {
639
- const signature = signaturesByIndex.get(idx);
640
- if (signature === undefined) {
641
- throw new Error(`Missing signature for index ${idx}`);
642
- }
643
- return signature;
644
- });
645
-
646
- // Aggregate signatures (equivalent to tbls.ThresholdAggregate in Go)
647
- // Note: @chainsafe/bls doesn't have explicit threshold aggregation, but ordering should be preserved
648
- const fullSig = aggregateSignatures(rawSignatures);
649
-
650
- return {
651
- public_key: exitBlob.public_key,
652
- signed_exit_message: {
653
- message: {
654
- epoch: exitBlob.epoch,
655
- validator_index: exitBlob.validator_index,
656
- },
657
- signature: '0x' + Buffer.from(fullSig).toString('hex'),
658
- },
659
- };
660
- }
661
- }