@cofhe/sdk 0.3.2 → 0.5.0

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 (97) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/adapters/{ethers5.test.ts → test/ethers5.test.ts} +2 -2
  3. package/adapters/{ethers6.test.ts → test/ethers6.test.ts} +2 -2
  4. package/adapters/{hardhat.hh2.test.ts → test/hardhat.hh2.test.ts} +2 -2
  5. package/adapters/{index.test.ts → test/index.test.ts} +1 -1
  6. package/adapters/{wagmi.test.ts → test/wagmi.test.ts} +1 -1
  7. package/chains/{chains.test.ts → test/chains.test.ts} +1 -1
  8. package/core/client.ts +15 -5
  9. package/core/clientTypes.ts +7 -5
  10. package/core/consts.ts +9 -0
  11. package/core/decrypt/cofheMocksDecryptForTx.ts +14 -3
  12. package/core/decrypt/decryptForTxBuilder.ts +24 -10
  13. package/core/decrypt/decryptForViewBuilder.ts +14 -7
  14. package/core/decrypt/polling.ts +14 -0
  15. package/core/decrypt/tnDecryptUtils.ts +65 -0
  16. package/core/decrypt/{tnDecrypt.ts → tnDecryptV1.ts} +7 -70
  17. package/core/decrypt/tnDecryptV2.ts +483 -0
  18. package/core/decrypt/tnSealOutputV2.ts +245 -104
  19. package/core/decrypt/verifyDecryptResult.ts +65 -0
  20. package/core/encrypt/cofheMocksZkVerifySign.ts +6 -6
  21. package/core/encrypt/zkPackProveVerify.ts +10 -19
  22. package/core/fetchKeys.ts +0 -2
  23. package/core/index.ts +9 -1
  24. package/core/keyStore.ts +5 -2
  25. package/core/permits.ts +8 -3
  26. package/core/{client.test.ts → test/client.test.ts} +7 -7
  27. package/core/{config.test.ts → test/config.test.ts} +1 -1
  28. package/core/test/decrypt.test.ts +252 -0
  29. package/core/test/decryptBuilders.test.ts +390 -0
  30. package/core/{encrypt → test}/encryptInputsBuilder.test.ts +61 -6
  31. package/core/{fetchKeys.test.ts → test/fetchKeys.test.ts} +3 -3
  32. package/core/{keyStore.test.ts → test/keyStore.test.ts} +5 -3
  33. package/core/{permits.test.ts → test/permits.test.ts} +42 -1
  34. package/core/test/pollCallbacks.test.ts +563 -0
  35. package/core/types.ts +21 -0
  36. package/dist/chains.d.cts +2 -2
  37. package/dist/chains.d.ts +2 -2
  38. package/dist/chunk-4FP4V35O.js +13 -0
  39. package/dist/{chunk-NWDKXBIP.js → chunk-MRCKUMOS.js} +62 -22
  40. package/dist/{chunk-LWMRB6SD.js → chunk-S7OKGLFD.js} +615 -198
  41. package/dist/{clientTypes-Y43CKbOz.d.cts → clientTypes-BSbwairE.d.cts} +38 -13
  42. package/dist/{clientTypes-PQha8zes.d.ts → clientTypes-DDmcgZ0a.d.ts} +38 -13
  43. package/dist/core.cjs +691 -235
  44. package/dist/core.d.cts +24 -6
  45. package/dist/core.d.ts +24 -6
  46. package/dist/core.js +3 -2
  47. package/dist/node.cjs +696 -237
  48. package/dist/node.d.cts +3 -3
  49. package/dist/node.d.ts +3 -3
  50. package/dist/node.js +14 -7
  51. package/dist/{permit-MZ502UBl.d.ts → permit-DnVMDT5h.d.cts} +34 -4
  52. package/dist/{permit-MZ502UBl.d.cts → permit-DnVMDT5h.d.ts} +34 -4
  53. package/dist/permits.cjs +66 -29
  54. package/dist/permits.d.cts +18 -13
  55. package/dist/permits.d.ts +18 -13
  56. package/dist/permits.js +2 -1
  57. package/dist/web.cjs +718 -242
  58. package/dist/web.d.cts +8 -4
  59. package/dist/web.d.ts +8 -4
  60. package/dist/web.js +34 -11
  61. package/dist/zkProve.worker.cjs +6 -3
  62. package/dist/zkProve.worker.js +5 -3
  63. package/node/index.ts +13 -4
  64. package/node/test/client.test.ts +25 -0
  65. package/node/test/config.test.ts +16 -0
  66. package/node/test/inherited.test.ts +244 -0
  67. package/node/test/tfheinit.test.ts +56 -0
  68. package/package.json +24 -22
  69. package/permits/permit.ts +31 -5
  70. package/permits/sealing.ts +1 -1
  71. package/permits/{localstorage.test.ts → test/localstorage.test.ts} +2 -2
  72. package/permits/{permit.test.ts → test/permit.test.ts} +35 -1
  73. package/permits/{sealing.test.ts → test/sealing.test.ts} +1 -1
  74. package/permits/{store.test.ts → test/store.test.ts} +2 -2
  75. package/permits/{validation.test.ts → test/validation.test.ts} +82 -6
  76. package/permits/types.ts +1 -1
  77. package/permits/validation.ts +42 -2
  78. package/web/const.ts +2 -0
  79. package/web/index.ts +20 -6
  80. package/web/storage.ts +18 -3
  81. package/web/{client.web.test.ts → test/client.web.test.ts} +13 -1
  82. package/web/test/config.web.test.ts +16 -0
  83. package/web/test/inherited.web.test.ts +245 -0
  84. package/web/test/tfheinit.web.test.ts +62 -0
  85. package/web/{worker.config.web.test.ts → test/worker.config.web.test.ts} +1 -1
  86. package/web/{worker.output.web.test.ts → test/worker.output.web.test.ts} +1 -1
  87. package/web/{workerManager.test.ts → test/workerManager.test.ts} +1 -1
  88. package/web/{workerManager.web.test.ts → test/workerManager.web.test.ts} +1 -1
  89. package/web/zkProve.worker.ts +4 -3
  90. package/node/client.test.ts +0 -147
  91. package/node/config.test.ts +0 -68
  92. package/node/encryptInputs.test.ts +0 -155
  93. package/web/config.web.test.ts +0 -69
  94. package/web/encryptInputs.web.test.ts +0 -172
  95. package/web/worker.builder.web.test.ts +0 -148
  96. /package/dist/{types-YiAC4gig.d.cts → types-C07FK-cL.d.cts} +0 -0
  97. /package/dist/{types-YiAC4gig.d.ts → types-C07FK-cL.d.ts} +0 -0
@@ -1,16 +1,38 @@
1
1
  import { type Permission, type EthEncryptedData } from '@/permits';
2
2
 
3
3
  import { CofheError, CofheErrorCode } from '../error.js';
4
+ import { type DecryptPollCallbackFunction } from '../types.js';
5
+ import { computeMinuteRampPollIntervalMs } from './polling.js';
4
6
 
5
7
  // Polling configuration
6
8
  const POLL_INTERVAL_MS = 1000; // 1 second
7
- const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
9
+ const POLL_MAX_INTERVAL_MS = 10_000; // 10 seconds
10
+ const SEAL_OUTPUT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total across submit + poll
11
+ const SUBMIT_RETRY_INTERVAL_MS = 1000; // 1 second
8
12
 
9
13
  // V2 API response types
10
14
  type SealOutputSubmitResponse = {
11
- request_id: string;
15
+ request_id: string | null;
16
+ status?: string;
17
+ is_succeed?: boolean;
18
+ sealed?: {
19
+ data: number[];
20
+ public_key: number[];
21
+ nonce: number[];
22
+ };
23
+ sealed_data?: number[];
24
+ ephemeral_public_key?: number[];
25
+ nonce?: number[];
26
+ signature?: string;
27
+ encryption_type?: number;
28
+ error_message?: string | null;
29
+ message?: string;
12
30
  };
13
31
 
32
+ type SealOutputSubmitResult =
33
+ | { kind: 'request_id'; requestId: string }
34
+ | { kind: 'completed'; sealed: EthEncryptedData };
35
+
14
36
  type SealOutputStatusResponse = {
15
37
  request_id: string;
16
38
  status: 'PROCESSING' | 'COMPLETED';
@@ -52,83 +74,192 @@ function convertSealedData(sealed: SealOutputStatusResponse['sealed']): EthEncry
52
74
  };
53
75
  }
54
76
 
55
- /**
56
- * Submits a sealoutput request to the v2 API and returns the request_id
57
- */
58
- async function submitSealOutputRequest(
59
- thresholdNetworkUrl: string,
60
- ctHash: bigint | string,
61
- chainId: number,
62
- permission: Permission
63
- ): Promise<string> {
64
- const body = {
65
- ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
66
- host_chain_id: chainId,
67
- permit: permission,
68
- };
77
+ function getSealedDataFromSubmitResponse(
78
+ value: SealOutputSubmitResponse
79
+ ): SealOutputStatusResponse['sealed'] | undefined {
80
+ if (value.sealed) return value.sealed;
69
81
 
70
- let response: Response;
71
- try {
72
- response = await fetch(`${thresholdNetworkUrl}/v2/sealoutput`, {
73
- method: 'POST',
74
- headers: {
75
- 'Content-Type': 'application/json',
76
- },
77
- body: JSON.stringify(body),
78
- });
79
- } catch (e) {
80
- throw new CofheError({
81
- code: CofheErrorCode.SealOutputFailed,
82
- message: `sealOutput request failed`,
83
- hint: 'Ensure the threshold network URL is valid and reachable.',
84
- cause: e instanceof Error ? e : undefined,
85
- context: {
86
- thresholdNetworkUrl,
87
- body,
88
- },
89
- });
82
+ if (Array.isArray(value.sealed_data) && Array.isArray(value.ephemeral_public_key) && Array.isArray(value.nonce)) {
83
+ return {
84
+ data: value.sealed_data,
85
+ public_key: value.ephemeral_public_key,
86
+ nonce: value.nonce,
87
+ };
90
88
  }
91
89
 
92
- // Handle non-200 status codes
93
- if (!response.ok) {
94
- let errorMessage = `HTTP ${response.status}`;
95
- try {
96
- const errorBody = await response.json();
97
- errorMessage = errorBody.error_message || errorBody.message || errorMessage;
98
- } catch {
99
- // Ignore JSON parse errors, use status text
100
- errorMessage = response.statusText || errorMessage;
101
- }
90
+ return undefined;
91
+ }
102
92
 
93
+ function parseCompletedSealOutputResponse(params: {
94
+ value: Pick<SealOutputStatusResponse, 'sealed' | 'error_message' | 'is_succeed'>;
95
+ thresholdNetworkUrl: string;
96
+ requestId?: string | null;
97
+ }): EthEncryptedData {
98
+ const { value, thresholdNetworkUrl, requestId } = params;
99
+
100
+ if (value.is_succeed === false) {
101
+ const errorMessage = value.error_message || 'Unknown error';
103
102
  throw new CofheError({
104
103
  code: CofheErrorCode.SealOutputFailed,
105
104
  message: `sealOutput request failed: ${errorMessage}`,
106
- hint: 'Check the threshold network URL and request parameters.',
107
105
  context: {
108
106
  thresholdNetworkUrl,
109
- status: response.status,
110
- statusText: response.statusText,
111
- body,
107
+ requestId,
108
+ response: value,
112
109
  },
113
110
  });
114
111
  }
115
112
 
116
- let submitResponse: SealOutputSubmitResponse;
117
- try {
118
- submitResponse = (await response.json()) as SealOutputSubmitResponse;
119
- } catch (e) {
113
+ const sealed = 'sealed' in value ? value.sealed : getSealedDataFromSubmitResponse(value as SealOutputSubmitResponse);
114
+
115
+ if (!sealed) {
120
116
  throw new CofheError({
121
- code: CofheErrorCode.SealOutputFailed,
122
- message: `Failed to parse sealOutput submit response`,
123
- cause: e instanceof Error ? e : undefined,
117
+ code: CofheErrorCode.SealOutputReturnedNull,
118
+ message: `sealOutput request completed but returned no sealed data`,
124
119
  context: {
125
120
  thresholdNetworkUrl,
126
- body,
121
+ requestId,
122
+ response: value,
127
123
  },
128
124
  });
129
125
  }
130
126
 
131
- if (!submitResponse.request_id) {
127
+ return convertSealedData(sealed);
128
+ }
129
+
130
+ /**
131
+ * Submits a sealoutput request to the v2 API and returns the request_id
132
+ */
133
+ async function submitSealOutputRequest(
134
+ thresholdNetworkUrl: string,
135
+ ctHash: bigint | string,
136
+ chainId: number,
137
+ permission: Permission,
138
+ overallStartTime: number,
139
+ onPoll?: DecryptPollCallbackFunction
140
+ ): Promise<SealOutputSubmitResult> {
141
+ const body = {
142
+ ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
143
+ host_chain_id: chainId,
144
+ permit: permission,
145
+ };
146
+ let attemptIndex = 0;
147
+
148
+ for (;;) {
149
+ let response: Response;
150
+ try {
151
+ response = await fetch(`${thresholdNetworkUrl}/v2/sealoutput`, {
152
+ method: 'POST',
153
+ headers: {
154
+ 'Content-Type': 'application/json',
155
+ },
156
+ body: JSON.stringify(body),
157
+ });
158
+ } catch (e) {
159
+ throw new CofheError({
160
+ code: CofheErrorCode.SealOutputFailed,
161
+ message: `sealOutput request failed`,
162
+ hint: 'Ensure the threshold network URL is valid and reachable.',
163
+ cause: e instanceof Error ? e : undefined,
164
+ context: {
165
+ thresholdNetworkUrl,
166
+ body,
167
+ attemptIndex,
168
+ },
169
+ });
170
+ }
171
+
172
+ // Handle non-200 status codes
173
+ if (!response.ok) {
174
+ let errorMessage = `HTTP ${response.status}`;
175
+ try {
176
+ const errorBody = await response.json();
177
+
178
+ errorMessage = errorBody.error_message || errorBody.message || errorMessage;
179
+ } catch {
180
+ errorMessage = response.statusText || errorMessage;
181
+ }
182
+
183
+ throw new CofheError({
184
+ code: CofheErrorCode.SealOutputFailed,
185
+ message: `sealOutput request failed: ${errorMessage}`,
186
+ hint: 'Check the threshold network URL and request parameters.',
187
+ context: {
188
+ thresholdNetworkUrl,
189
+ status: response.status,
190
+ statusText: response.statusText,
191
+ body,
192
+ attemptIndex,
193
+ },
194
+ });
195
+ }
196
+
197
+ let submitResponse: SealOutputSubmitResponse | undefined;
198
+ if (response.status !== 204) {
199
+ try {
200
+ submitResponse = (await response.json()) as SealOutputSubmitResponse;
201
+ } catch (e) {
202
+ throw new CofheError({
203
+ code: CofheErrorCode.SealOutputFailed,
204
+ message: `Failed to parse sealOutput submit response`,
205
+ cause: e instanceof Error ? e : undefined,
206
+ context: {
207
+ thresholdNetworkUrl,
208
+ body,
209
+ attemptIndex,
210
+ },
211
+ });
212
+ }
213
+
214
+ if (getSealedDataFromSubmitResponse(submitResponse)) {
215
+ return {
216
+ kind: 'completed',
217
+ sealed: parseCompletedSealOutputResponse({
218
+ value: submitResponse,
219
+ thresholdNetworkUrl,
220
+ requestId: submitResponse.request_id,
221
+ }),
222
+ };
223
+ }
224
+
225
+ if (submitResponse.request_id) {
226
+ return { kind: 'request_id', requestId: submitResponse.request_id };
227
+ }
228
+ }
229
+
230
+ // 204 means backend is aware of ct hash but didn't calculate it yet
231
+ if (response.status === 204) {
232
+ const elapsedMs = Date.now() - overallStartTime;
233
+ if (elapsedMs > SEAL_OUTPUT_TIMEOUT_MS) {
234
+ throw new CofheError({
235
+ code: CofheErrorCode.SealOutputFailed,
236
+ message: `sealOutput submit retried without receiving request_id for ${SEAL_OUTPUT_TIMEOUT_MS}ms`,
237
+ hint: 'The ciphertext may still be propagating. Try again later.',
238
+ context: {
239
+ thresholdNetworkUrl,
240
+ body,
241
+ attemptIndex,
242
+ timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
243
+ submitResponse,
244
+ status: response.status,
245
+ },
246
+ });
247
+ }
248
+
249
+ onPoll?.({
250
+ operation: 'sealoutput',
251
+ requestId: '',
252
+ attemptIndex,
253
+ elapsedMs,
254
+ intervalMs: SUBMIT_RETRY_INTERVAL_MS,
255
+ timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
256
+ });
257
+
258
+ await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
259
+ attemptIndex += 1;
260
+ continue;
261
+ }
262
+
132
263
  throw new CofheError({
133
264
  code: CofheErrorCode.SealOutputFailed,
134
265
  message: `sealOutput submit response missing request_id`,
@@ -136,31 +267,49 @@ async function submitSealOutputRequest(
136
267
  thresholdNetworkUrl,
137
268
  body,
138
269
  submitResponse,
270
+ attemptIndex,
139
271
  },
140
272
  });
141
273
  }
142
-
143
- return submitResponse.request_id;
144
274
  }
145
275
 
146
276
  /**
147
277
  * Polls for the sealoutput status until completed or timeout
148
278
  */
149
- async function pollSealOutputStatus(thresholdNetworkUrl: string, requestId: string): Promise<EthEncryptedData> {
150
- const startTime = Date.now();
279
+ async function pollSealOutputStatus(
280
+ thresholdNetworkUrl: string,
281
+ requestId: string,
282
+ overallStartTime: number,
283
+ onPoll?: DecryptPollCallbackFunction
284
+ ): Promise<EthEncryptedData> {
285
+ let attemptIndex = 0;
151
286
  let completed = false;
152
287
 
153
288
  while (!completed) {
289
+ const elapsedMs = Date.now() - overallStartTime;
290
+ const intervalMs = computeMinuteRampPollIntervalMs(elapsedMs, {
291
+ minIntervalMs: POLL_INTERVAL_MS,
292
+ maxIntervalMs: POLL_MAX_INTERVAL_MS,
293
+ });
294
+ onPoll?.({
295
+ operation: 'sealoutput',
296
+ requestId,
297
+ attemptIndex,
298
+ elapsedMs,
299
+ intervalMs,
300
+ timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
301
+ });
302
+
154
303
  // Check timeout
155
- if (Date.now() - startTime > POLL_TIMEOUT_MS) {
304
+ if (elapsedMs > SEAL_OUTPUT_TIMEOUT_MS) {
156
305
  throw new CofheError({
157
306
  code: CofheErrorCode.SealOutputFailed,
158
- message: `sealOutput polling timed out after ${POLL_TIMEOUT_MS}ms`,
307
+ message: `sealOutput polling timed out after ${SEAL_OUTPUT_TIMEOUT_MS}ms`,
159
308
  hint: 'The request may still be processing. Try again later.',
160
309
  context: {
161
310
  thresholdNetworkUrl,
162
311
  requestId,
163
- timeoutMs: POLL_TIMEOUT_MS,
312
+ timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
164
313
  },
165
314
  });
166
315
  }
@@ -238,39 +387,16 @@ async function pollSealOutputStatus(thresholdNetworkUrl: string, requestId: stri
238
387
 
239
388
  // Check if completed
240
389
  if (statusResponse.status === 'COMPLETED') {
241
- // Check if succeeded
242
- if (statusResponse.is_succeed === false) {
243
- const errorMessage = statusResponse.error_message || 'Unknown error';
244
- throw new CofheError({
245
- code: CofheErrorCode.SealOutputFailed,
246
- message: `sealOutput request failed: ${errorMessage}`,
247
- context: {
248
- thresholdNetworkUrl,
249
- requestId,
250
- statusResponse,
251
- },
252
- });
253
- }
254
-
255
- // Check if sealed data exists
256
- if (!statusResponse.sealed) {
257
- throw new CofheError({
258
- code: CofheErrorCode.SealOutputReturnedNull,
259
- message: `sealOutput request completed but returned no sealed data`,
260
- context: {
261
- thresholdNetworkUrl,
262
- requestId,
263
- statusResponse,
264
- },
265
- });
266
- }
267
-
268
- // Convert and return the sealed data
269
- return convertSealedData(statusResponse.sealed);
390
+ return parseCompletedSealOutputResponse({
391
+ value: statusResponse,
392
+ thresholdNetworkUrl,
393
+ requestId,
394
+ });
270
395
  }
271
396
 
272
397
  // Still processing, wait before next poll
273
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
398
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
399
+ attemptIndex += 1;
274
400
  }
275
401
 
276
402
  // This should never be reached, but TypeScript requires it
@@ -284,15 +410,30 @@ async function pollSealOutputStatus(thresholdNetworkUrl: string, requestId: stri
284
410
  });
285
411
  }
286
412
 
287
- export async function tnSealOutputV2(
288
- ctHash: bigint | string,
289
- chainId: number,
290
- permission: Permission,
291
- thresholdNetworkUrl: string
292
- ): Promise<EthEncryptedData> {
413
+ export async function tnSealOutputV2(params: {
414
+ ctHash: bigint | string;
415
+ chainId: number;
416
+ permission: Permission;
417
+ thresholdNetworkUrl: string;
418
+ onPoll?: DecryptPollCallbackFunction;
419
+ }): Promise<EthEncryptedData> {
420
+ const { thresholdNetworkUrl, ctHash, chainId, permission, onPoll } = params;
421
+ const overallStartTime = Date.now();
422
+
293
423
  // Step 1: Submit the request and get request_id
294
- const requestId = await submitSealOutputRequest(thresholdNetworkUrl, ctHash, chainId, permission);
424
+ const submitResult = await submitSealOutputRequest(
425
+ thresholdNetworkUrl,
426
+ ctHash,
427
+ chainId,
428
+ permission,
429
+ overallStartTime,
430
+ onPoll
431
+ );
432
+
433
+ if (submitResult.kind === 'completed') {
434
+ return submitResult.sealed;
435
+ }
295
436
 
296
437
  // Step 2: Poll for status until completed
297
- return await pollSealOutputStatus(thresholdNetworkUrl, requestId);
438
+ return await pollSealOutputStatus(thresholdNetworkUrl, submitResult.requestId, overallStartTime, onPoll);
298
439
  }
@@ -0,0 +1,65 @@
1
+ import {
2
+ encodePacked,
3
+ isAddressEqual,
4
+ keccak256,
5
+ parseAbi,
6
+ recoverAddress,
7
+ zeroAddress,
8
+ type Address,
9
+ type Hex,
10
+ type PublicClient,
11
+ } from 'viem';
12
+ import { TASK_MANAGER_ADDRESS } from '../consts.js';
13
+
14
+ const decryptResultSignerAbi = parseAbi(['function decryptResultSigner() view returns (address)']);
15
+ const UINT_TYPE_MASK = 0x7fn;
16
+ const TYPE_BYTE_OFFSET = 8n;
17
+
18
+ const getEncryptionTypeFromCtHash = (ctHash: bigint) => Number((ctHash >> TYPE_BYTE_OFFSET) & UINT_TYPE_MASK);
19
+
20
+ const buildDecryptResultHash = (ctHash: bigint, cleartext: bigint, chainId: number) => {
21
+ const encryptionType = getEncryptionTypeFromCtHash(ctHash);
22
+
23
+ return keccak256(
24
+ encodePacked(['uint256', 'uint32', 'uint64', 'uint256'], [cleartext, encryptionType, BigInt(chainId), ctHash])
25
+ );
26
+ };
27
+
28
+ /**
29
+ * Verifies a decrypt result signature **locally** (no `ctHash`/plaintext sent over RPC).
30
+ *
31
+ * The recovered signer must equal the on-chain configured `decryptResultSigner`.
32
+ *
33
+ * This mirrors the TaskManager decrypt-result hash format:
34
+ * `keccak256(abi.encodePacked(result, encType, chainId, ctHash))`
35
+ *
36
+ * The only on-chain read performed is `TaskManager.decryptResultSigner()` (via `eth_call`).
37
+ *
38
+ * Works with both production and mock deployments.
39
+ */
40
+ export async function verifyDecryptResult(
41
+ handle: bigint | string,
42
+ cleartext: bigint,
43
+ signature: Hex,
44
+ publicClient: PublicClient
45
+ ): Promise<boolean> {
46
+ const chainId = publicClient.chain?.id ?? (await publicClient.getChainId());
47
+ const expectedSigner = await publicClient.readContract({
48
+ address: TASK_MANAGER_ADDRESS,
49
+ abi: decryptResultSignerAbi,
50
+ functionName: 'decryptResultSigner',
51
+ args: [],
52
+ });
53
+
54
+ if (isAddressEqual(expectedSigner, zeroAddress)) return true;
55
+
56
+ const ctHash = BigInt(handle);
57
+ const messageHash = buildDecryptResultHash(ctHash, cleartext, chainId);
58
+
59
+ try {
60
+ const recovered = await recoverAddress({ hash: messageHash, signature });
61
+ return isAddressEqual(recovered, expectedSigner);
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
@@ -1,5 +1,5 @@
1
1
  import { type EncryptableItem, FheTypes } from '../types.js';
2
- import { MAX_ENCRYPTABLE_BITS, type VerifyResult } from './zkPackProveVerify.js';
2
+ import { type VerifyResult } from './zkPackProveVerify.js';
3
3
  import {
4
4
  createWalletClient,
5
5
  http,
@@ -14,7 +14,7 @@ import { MockZkVerifierAbi } from './MockZkVerifierAbi.js';
14
14
  import { hardhat } from 'viem/chains';
15
15
  import { CofheError, CofheErrorCode } from '../error.js';
16
16
  import { privateKeyToAccount, sign } from 'viem/accounts';
17
- import { MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY, MOCKS_ZK_VERIFIER_ADDRESS } from '../consts.js';
17
+ import { MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY, MOCKS_ZK_VERIFIER_ADDRESS, TFHE_RS_ZK_MAX_BITS } from '../consts.js';
18
18
 
19
19
  type EncryptableItemWithCtHash = EncryptableItem & {
20
20
  ctHash: bigint;
@@ -69,14 +69,14 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
69
69
  }
70
70
  }
71
71
  }
72
- if (totalBits > MAX_ENCRYPTABLE_BITS) {
72
+ if (totalBits > TFHE_RS_ZK_MAX_BITS) {
73
73
  throw new CofheError({
74
74
  code: CofheErrorCode.ZkPackFailed,
75
- message: `Total bits ${totalBits} exceeds ${MAX_ENCRYPTABLE_BITS}`,
76
- hint: `Ensure that the total bits of the items to encrypt does not exceed ${MAX_ENCRYPTABLE_BITS}`,
75
+ message: `Total bits ${totalBits} exceeds ${TFHE_RS_ZK_MAX_BITS}`,
76
+ hint: `Ensure that the total bits of the items to encrypt does not exceed ${TFHE_RS_ZK_MAX_BITS}`,
77
77
  context: {
78
78
  totalBits,
79
- maxBits: MAX_ENCRYPTABLE_BITS,
79
+ maxBits: TFHE_RS_ZK_MAX_BITS,
80
80
  items,
81
81
  },
82
82
  });
@@ -1,6 +1,7 @@
1
- import { CofheError, CofheErrorCode } from '../error.js';
2
- import { type EncryptableItem, FheTypes } from '../types.js';
3
- import { toBigIntOrThrow, validateBigIntInRange, toHexString, hexToBytes } from '../utils.js';
1
+ import { TFHE_RS_ZK_MAX_BITS, TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT } from '../consts';
2
+ import { CofheError, CofheErrorCode } from '../error';
3
+ import { type EncryptableItem, FheTypes } from '../types';
4
+ import { toBigIntOrThrow, validateBigIntInRange, toHexString, hexToBytes } from '../utils';
4
5
 
5
6
  // ===== TYPES =====
6
7
 
@@ -52,23 +53,14 @@ export type VerifyResult = {
52
53
  };
53
54
 
54
55
  export type ZkProvenCiphertextList = {
55
- serialize(): Uint8Array;
56
+ safe_serialize(serialized_size_limit: bigint): Uint8Array;
56
57
  };
57
58
 
58
59
  export type ZkCompactPkeCrs = {
59
60
  free(): void;
60
- serialize(compress: boolean): Uint8Array;
61
61
  safe_serialize(serialized_size_limit: bigint): Uint8Array;
62
62
  };
63
63
 
64
- export type ZkCompactPkeCrsConstructor = {
65
- deserialize(buffer: Uint8Array): ZkCompactPkeCrs;
66
- safe_deserialize(buffer: Uint8Array, serialized_size_limit: bigint): ZkCompactPkeCrs;
67
- from_config(config: unknown, max_num_bits: number): ZkCompactPkeCrs;
68
- deserialize_from_public_params(buffer: Uint8Array): ZkCompactPkeCrs;
69
- safe_deserialize_from_public_params(buffer: Uint8Array, serialized_size_limit: bigint): ZkCompactPkeCrs;
70
- };
71
-
72
64
  export type ZkCiphertextListBuilder = {
73
65
  push_boolean(data: boolean): void;
74
66
  push_u8(data: number): void;
@@ -98,7 +90,6 @@ export const MAX_UINT64: bigint = 18446744073709551615n; // 2^64 - 1
98
90
  export const MAX_UINT128: bigint = 340282366920938463463374607431768211455n; // 2^128 - 1
99
91
  export const MAX_UINT256: bigint = 115792089237316195423570985008687907853269984665640564039457584007913129640319n; // 2^256 - 1
100
92
  export const MAX_UINT160: bigint = 1461501637330902918203684832716283019655932542975n; // 2^160 - 1
101
- export const MAX_ENCRYPTABLE_BITS: number = 2048;
102
93
 
103
94
  // ===== CORE FUNCTIONS =====
104
95
 
@@ -174,14 +165,14 @@ export const zkPack = (items: EncryptableItem[], builder: ZkCiphertextListBuilde
174
165
  }
175
166
  }
176
167
 
177
- if (totalBits > MAX_ENCRYPTABLE_BITS) {
168
+ if (totalBits > TFHE_RS_ZK_MAX_BITS) {
178
169
  throw new CofheError({
179
170
  code: CofheErrorCode.ZkPackFailed,
180
- message: `Total bits ${totalBits} exceeds ${MAX_ENCRYPTABLE_BITS}`,
181
- hint: `Ensure that the total bits of the items to encrypt does not exceed ${MAX_ENCRYPTABLE_BITS}`,
171
+ message: `Total bits ${totalBits} exceeds ${TFHE_RS_ZK_MAX_BITS}`,
172
+ hint: `Ensure that the total bits of the items to encrypt does not exceed ${TFHE_RS_ZK_MAX_BITS}`,
182
173
  context: {
183
174
  totalBits,
184
- maxBits: MAX_ENCRYPTABLE_BITS,
175
+ maxBits: TFHE_RS_ZK_MAX_BITS,
185
176
  items,
186
177
  },
187
178
  });
@@ -231,7 +222,7 @@ export const zkProve = async (
231
222
  1 // ZkComputeLoad.Verify
232
223
  );
233
224
 
234
- resolve(compactList.serialize());
225
+ resolve(compactList.safe_serialize(TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT));
235
226
  }, 0);
236
227
  });
237
228
  };
package/core/fetchKeys.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { hardhat } from '@/chains';
2
-
3
1
  import { type CofheConfig, getCoFheUrlOrThrow } from './config.js';
4
2
  import { type KeysStorage } from './keyStore.js';
5
3
 
package/core/index.ts CHANGED
@@ -43,6 +43,9 @@ export type {
43
43
  FheTypeValue,
44
44
  // Decryption types
45
45
  UnsealedItem,
46
+ DecryptPollCallbackFunction,
47
+ DecryptPollCallbackContext,
48
+ DecryptEndpoint,
46
49
  // Util types
47
50
  EncryptStepCallbackFunction as EncryptSetStateFn,
48
51
  EncryptStepCallbackContext,
@@ -85,7 +88,7 @@ export type {
85
88
  } from './encrypt/zkPackProveVerify.js';
86
89
  export { zkProveWithWorker } from './encrypt/zkPackProveVerify.js';
87
90
 
88
- // Contract addresses
91
+ // Consts (contract addresses, serialized size limits)
89
92
  export {
90
93
  TASK_MANAGER_ADDRESS,
91
94
  MOCKS_ZK_VERIFIER_ADDRESS,
@@ -94,7 +97,12 @@ export {
94
97
  MOCKS_DECRYPT_RESULT_SIGNER_PRIVATE_KEY,
95
98
  MOCKS_THRESHOLD_NETWORK_ADDRESS,
96
99
  TEST_BED_ADDRESS,
100
+ TFHE_RS_ZK_MAX_BITS,
101
+ TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT,
97
102
  } from './consts.js';
98
103
 
104
+ // Decrypt result verification
105
+ export { verifyDecryptResult } from './decrypt/verifyDecryptResult.js';
106
+
99
107
  // Utils
100
108
  export { fheTypeToString } from './utils.js';
package/core/keyStore.ts CHANGED
@@ -2,6 +2,7 @@ import { createStore, type StoreApi } from 'zustand/vanilla';
2
2
  import { persist, createJSONStorage } from 'zustand/middleware';
3
3
  import { produce } from 'immer';
4
4
  import { type IStorage } from './types.js';
5
+ import { TFHE_RS_KEY_VERSION } from './consts.js';
5
6
 
6
7
  // Type definitions
7
8
  type ChainRecord<T> = Record<string, T>;
@@ -23,6 +24,8 @@ export type KeysStorage = {
23
24
  rehydrateKeysStore: () => Promise<void>;
24
25
  };
25
26
 
27
+ const KEYSTORE_NAME = `cofhesdk-keys-v${TFHE_RS_KEY_VERSION}`;
28
+
26
29
  function isValidPersistedState(state: unknown): state is KeysStore {
27
30
  if (state && typeof state === 'object') {
28
31
  if ('fhe' in state && 'crs' in state) {
@@ -94,7 +97,7 @@ export function createKeysStore(storage: IStorage | null): KeysStorage {
94
97
 
95
98
  const clearKeysStorage = async () => {
96
99
  if (storage) {
97
- await storage.removeItem('cofhesdk-keys');
100
+ await storage.removeItem(KEYSTORE_NAME);
98
101
  }
99
102
  // If no storage, this is a no-op
100
103
  };
@@ -125,7 +128,7 @@ function createStoreWithPersit(storage: IStorage) {
125
128
  onRehydrateStorage: () => (_state?, _error?) => {
126
129
  if (_error) throw new Error(`onRehydrateStorage: Error rehydrating keys store: ${_error}`);
127
130
  },
128
- name: 'cofhesdk-keys',
131
+ name: KEYSTORE_NAME,
129
132
  storage: createJSONStorage(() => storage),
130
133
  merge: (persistedState, currentState) => {
131
134
  const persisted = isValidPersistedState(persistedState) ? persistedState : DEFAULT_KEYS_STORE;