@cofhe/sdk 0.7.1 → 1.0.0-alpha.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 (123) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/acps/acp.ts +35 -7
  3. package/acps/index.ts +28 -12
  4. package/acps/onchain-utils.ts +188 -49
  5. package/acps/registry.ts +231 -0
  6. package/acps/sealing.ts +9 -3
  7. package/acps/share-metadata/appKey.ts +8 -0
  8. package/acps/share-metadata/decode.ts +120 -0
  9. package/acps/share-metadata/describe.ts +198 -0
  10. package/acps/share-metadata/encode.ts +147 -0
  11. package/acps/share-metadata/erc7984.ts +26 -0
  12. package/acps/share-metadata/index.ts +9 -0
  13. package/acps/share-metadata/layout.ts +69 -0
  14. package/acps/share-metadata/presets.ts +116 -0
  15. package/acps/share-metadata/schema.ts +47 -0
  16. package/acps/share-metadata/test/codec.test.ts +140 -0
  17. package/acps/share-metadata/test/describe.test.ts +112 -0
  18. package/acps/share-metadata/test/fixtures.ts +157 -0
  19. package/acps/share-metadata/test/live.test.ts +56 -0
  20. package/acps/share-metadata/test/presets.test.ts +119 -0
  21. package/acps/share-metadata/test/verify.test.ts +144 -0
  22. package/acps/share-metadata/verify.ts +155 -0
  23. package/acps/store.ts +48 -18
  24. package/acps/test/acp.test.ts +102 -7
  25. package/acps/test/localstorage.test.ts +28 -1
  26. package/acps/test/onchainAccessStatus.test.ts +116 -0
  27. package/acps/test/registry.test.ts +144 -0
  28. package/acps/test/sealing.test.ts +23 -0
  29. package/acps/test/store.test.ts +71 -0
  30. package/acps/test/utils.test.ts +63 -0
  31. package/acps/test/validation.test.ts +0 -4
  32. package/acps/types.ts +59 -6
  33. package/acps/utils.ts +15 -2
  34. package/acps/validation.ts +3 -1
  35. package/chains/chains/arbitrum.ts +14 -0
  36. package/chains/chains/ethereum.ts +14 -0
  37. package/chains/index.ts +5 -1
  38. package/chains/test/chains.test.ts +24 -3
  39. package/core/acps.ts +164 -207
  40. package/core/client.ts +44 -13
  41. package/core/clientTypes.ts +50 -7
  42. package/core/config.ts +33 -1
  43. package/core/consts.ts +14 -2
  44. package/core/decrypt/apiError.ts +4 -1
  45. package/core/decrypt/verifyDecryptResult.ts +8 -4
  46. package/core/encrypt/cofheMocksZkVerifySign.ts +19 -2
  47. package/core/encrypt/encryptInputsBuilder.ts +6 -0
  48. package/core/encrypt/zkPackProveVerify.ts +7 -1
  49. package/core/error.ts +1 -0
  50. package/core/fetchKeys.ts +2 -2
  51. package/core/index.ts +9 -1
  52. package/core/keyStore.ts +23 -10
  53. package/core/taskManager.ts +48 -0
  54. package/core/test/acpDefaults.test.ts +108 -19
  55. package/core/test/acpStoreAccountCase.test.ts +80 -0
  56. package/core/test/acps.test.ts +128 -3
  57. package/core/test/apiError.test.ts +8 -0
  58. package/core/test/appKeyConfig.test.ts +94 -0
  59. package/core/test/client.test.ts +41 -0
  60. package/core/test/config.test.ts +24 -0
  61. package/core/test/decrypt.test.ts +11 -4
  62. package/core/test/decryptBuilders.test.ts +110 -2
  63. package/core/test/encryptInputsBuilder.test.ts +1 -1
  64. package/core/test/fetchKeys.test.ts +23 -2
  65. package/core/test/incomingShares.test.ts +84 -0
  66. package/core/test/keyStore.test.ts +18 -6
  67. package/core/test/mocksEncryptRange.test.ts +90 -0
  68. package/core/test/taskManager.test.ts +57 -0
  69. package/core/test/utils.test.ts +128 -0
  70. package/core/test/verifyDecryptResult.test.ts +51 -0
  71. package/core/types.ts +8 -0
  72. package/core/utils.ts +16 -9
  73. package/dist/acps.cjs +984 -64
  74. package/dist/acps.d.cts +589 -10
  75. package/dist/acps.d.ts +589 -10
  76. package/dist/acps.js +2 -2
  77. package/dist/chains.cjs +26 -0
  78. package/dist/chains.d.cts +45 -1
  79. package/dist/chains.d.ts +45 -1
  80. package/dist/chains.js +1 -1
  81. package/dist/chunk-6ZW76ZKB.js +228 -0
  82. package/dist/chunk-BQWHPIYU.js +1828 -0
  83. package/dist/chunk-KSOV5B4B.js +50 -0
  84. package/dist/{chunk-43USWPEH.js → chunk-OL3DZN7A.js} +159 -523
  85. package/dist/{chunk-N6IDQRRU.js → chunk-UKQAJK4N.js} +25 -1
  86. package/dist/chunk-XQ32JG3T.js +174 -0
  87. package/dist/{clientTypes-CYZjFznO.d.cts → clientTypes-C0A9ji5O.d.cts} +132 -29
  88. package/dist/{clientTypes-BN3nbzYM.d.ts → clientTypes-CcU5WvdV.d.ts} +132 -29
  89. package/dist/core.cjs +3269 -2626
  90. package/dist/core.d.cts +32 -8
  91. package/dist/core.d.ts +32 -8
  92. package/dist/core.js +5 -4
  93. package/dist/node.cjs +3074 -2436
  94. package/dist/node.d.cts +2 -2
  95. package/dist/node.d.ts +2 -2
  96. package/dist/node.js +20 -19
  97. package/dist/{acp-Wi6isVQI.d.cts → onchain-utils-D9Z33IP7.d.cts} +166 -12
  98. package/dist/{acp-Wi6isVQI.d.ts → onchain-utils-D9Z33IP7.d.ts} +166 -12
  99. package/dist/web.cjs +3204 -2493
  100. package/dist/web.d.cts +49 -3
  101. package/dist/web.d.ts +49 -3
  102. package/dist/web.js +52 -27
  103. package/dist/zkProve.worker.cjs +79 -18
  104. package/dist/zkProve.worker.js +24 -24
  105. package/node/index.ts +1 -10
  106. package/node/storage.ts +20 -9
  107. package/node/test/inherited.test.ts +5 -1
  108. package/node/test/storage-memory-fallback.test.ts +47 -0
  109. package/node/test/storage-permissions.test.ts +40 -0
  110. package/package.json +3 -1
  111. package/web/index.ts +101 -36
  112. package/web/test/countingZkProveWorker.ts +31 -0
  113. package/web/test/inherited.web.test.ts +5 -1
  114. package/web/test/mainThreadPool.web.test.ts +174 -0
  115. package/web/test/threadPoolBenchHelper.ts +134 -0
  116. package/web/test/threadPoolOff.web.test.ts +17 -0
  117. package/web/test/threadPoolOn.web.test.ts +80 -0
  118. package/web/test/workerInitRace.web.test.ts +79 -0
  119. package/web/tfheThreadPool.ts +140 -0
  120. package/web/workerManager.ts +4 -2
  121. package/web/zkProve.worker.ts +42 -26
  122. package/dist/chunk-ESMZCFJY.js +0 -12
  123. package/dist/chunk-Q7CBWGQX.js +0 -1029
package/dist/web.d.cts CHANGED
@@ -1,12 +1,17 @@
1
- import { I as IStorage, C as CofheInputConfig, a as CofheConfig, b as CofheClient, E as EncryptableItem } from './clientTypes-CYZjFznO.cjs';
1
+ import { I as IStorage, C as CofheInputConfig, a as CofheConfig, b as CofheClient, E as EncryptableItem } from './clientTypes-C0A9ji5O.cjs';
2
2
  import 'viem';
3
3
  import './types-C07FK-cL.cjs';
4
4
  import 'zod';
5
- import './acp-Wi6isVQI.cjs';
5
+ import './onchain-utils-D9Z33IP7.cjs';
6
6
  import 'zustand/vanilla';
7
7
 
8
8
  declare function createSsrStorage(): IStorage;
9
9
 
10
+ /**
11
+ * Worker Manager for ZK Proof Generation
12
+ * Manages worker lifecycle and request/response handling
13
+ */
14
+
10
15
  /**
11
16
  * Terminate the worker
12
17
  */
@@ -16,8 +21,49 @@ declare function terminateWorker(): void;
16
21
  */
17
22
  declare function areWorkersAvailable(): boolean;
18
23
 
24
+ /**
25
+ * Rayon thread pool setup for the browser build of tfhe.
26
+ *
27
+ * `tfhe` is built with wasm-bindgen-rayon, so the ZK proof (the
28
+ * `build_with_proof_packed` call, by far the most expensive step of
29
+ * `encryptInputs`) can run across several threads. Those threads are real Web
30
+ * Workers that share the wasm linear memory, and `initThreadPool` hands them
31
+ * that memory over `postMessage`.
32
+ *
33
+ * Sharing memory across threads is only allowed in a cross-origin-isolated
34
+ * context, so the host page must be served with:
35
+ *
36
+ * Cross-Origin-Opener-Policy: same-origin
37
+ * Cross-Origin-Embedder-Policy: require-corp (or `credentialless`)
38
+ *
39
+ * Plenty of apps can't or don't set those headers, and calling
40
+ * `initThreadPool` without them throws ("SharedArrayBuffer transfer requires
41
+ * self.crossOriginIsolated"). So this is strictly an optimisation: we probe for
42
+ * isolation, and when it isn't available we leave tfhe single-threaded rather
43
+ * than failing the encryption. Single-threaded tfhe works in every context.
44
+ */
45
+
46
+ type TfheThreadPoolResult = {
47
+ /** Whether a multi-threaded rayon pool is now running */
48
+ enabled: boolean;
49
+ /** Threads requested from `initThreadPool` (1 when single-threaded) */
50
+ threads: number;
51
+ /** Why the pool was not started, when `enabled` is false */
52
+ reason?: string;
53
+ };
54
+
19
55
  declare const hasDOM: boolean;
20
56
 
57
+ /**
58
+ * Outcome of the rayon thread pool setup on the main thread, or `null` if the
59
+ * main thread hasn't had to generate a proof yet. `enabled: false` carries a
60
+ * `reason`.
61
+ *
62
+ * Note this reflects the *main thread* only. When workers are enabled (the
63
+ * default) proving happens inside the zkProve worker, which runs its own pool,
64
+ * and the main thread never starts one — so `null` is the normal value there.
65
+ */
66
+ declare function getTfheThreadPoolStatus(): TfheThreadPoolResult | null;
21
67
  /**
22
68
  * Creates a CoFHE configuration for web with IndexedDB storage as default
23
69
  * @param config - The CoFHE input configuration (fheKeyStorage will default to IndexedDB if not provided)
@@ -39,4 +85,4 @@ declare function createCofheClient<TConfig extends CofheConfig>(config: TConfig)
39
85
  */
40
86
  declare function createCofheClientWithCustomWorker(config: CofheConfig, customZkProveWorkerFn: (fheKeyHex: string, crsHex: string, items: EncryptableItem[], metadata: Uint8Array) => Promise<Uint8Array>): CofheClient;
41
87
 
42
- export { areWorkersAvailable, createCofheClient, createCofheClientWithCustomWorker, createCofheConfig, createSsrStorage, hasDOM, terminateWorker };
88
+ export { areWorkersAvailable, createCofheClient, createCofheClientWithCustomWorker, createCofheConfig, createSsrStorage, getTfheThreadPoolStatus, hasDOM, terminateWorker };
package/dist/web.d.ts CHANGED
@@ -1,12 +1,17 @@
1
- import { I as IStorage, C as CofheInputConfig, a as CofheConfig, b as CofheClient, E as EncryptableItem } from './clientTypes-BN3nbzYM.js';
1
+ import { I as IStorage, C as CofheInputConfig, a as CofheConfig, b as CofheClient, E as EncryptableItem } from './clientTypes-CcU5WvdV.js';
2
2
  import 'viem';
3
3
  import './types-C07FK-cL.js';
4
4
  import 'zod';
5
- import './acp-Wi6isVQI.js';
5
+ import './onchain-utils-D9Z33IP7.js';
6
6
  import 'zustand/vanilla';
7
7
 
8
8
  declare function createSsrStorage(): IStorage;
9
9
 
10
+ /**
11
+ * Worker Manager for ZK Proof Generation
12
+ * Manages worker lifecycle and request/response handling
13
+ */
14
+
10
15
  /**
11
16
  * Terminate the worker
12
17
  */
@@ -16,8 +21,49 @@ declare function terminateWorker(): void;
16
21
  */
17
22
  declare function areWorkersAvailable(): boolean;
18
23
 
24
+ /**
25
+ * Rayon thread pool setup for the browser build of tfhe.
26
+ *
27
+ * `tfhe` is built with wasm-bindgen-rayon, so the ZK proof (the
28
+ * `build_with_proof_packed` call, by far the most expensive step of
29
+ * `encryptInputs`) can run across several threads. Those threads are real Web
30
+ * Workers that share the wasm linear memory, and `initThreadPool` hands them
31
+ * that memory over `postMessage`.
32
+ *
33
+ * Sharing memory across threads is only allowed in a cross-origin-isolated
34
+ * context, so the host page must be served with:
35
+ *
36
+ * Cross-Origin-Opener-Policy: same-origin
37
+ * Cross-Origin-Embedder-Policy: require-corp (or `credentialless`)
38
+ *
39
+ * Plenty of apps can't or don't set those headers, and calling
40
+ * `initThreadPool` without them throws ("SharedArrayBuffer transfer requires
41
+ * self.crossOriginIsolated"). So this is strictly an optimisation: we probe for
42
+ * isolation, and when it isn't available we leave tfhe single-threaded rather
43
+ * than failing the encryption. Single-threaded tfhe works in every context.
44
+ */
45
+
46
+ type TfheThreadPoolResult = {
47
+ /** Whether a multi-threaded rayon pool is now running */
48
+ enabled: boolean;
49
+ /** Threads requested from `initThreadPool` (1 when single-threaded) */
50
+ threads: number;
51
+ /** Why the pool was not started, when `enabled` is false */
52
+ reason?: string;
53
+ };
54
+
19
55
  declare const hasDOM: boolean;
20
56
 
57
+ /**
58
+ * Outcome of the rayon thread pool setup on the main thread, or `null` if the
59
+ * main thread hasn't had to generate a proof yet. `enabled: false` carries a
60
+ * `reason`.
61
+ *
62
+ * Note this reflects the *main thread* only. When workers are enabled (the
63
+ * default) proving happens inside the zkProve worker, which runs its own pool,
64
+ * and the main thread never starts one — so `null` is the normal value there.
65
+ */
66
+ declare function getTfheThreadPoolStatus(): TfheThreadPoolResult | null;
21
67
  /**
22
68
  * Creates a CoFHE configuration for web with IndexedDB storage as default
23
69
  * @param config - The CoFHE input configuration (fheKeyStorage will default to IndexedDB if not provided)
@@ -39,4 +85,4 @@ declare function createCofheClient<TConfig extends CofheConfig>(config: TConfig)
39
85
  */
40
86
  declare function createCofheClientWithCustomWorker(config: CofheConfig, customZkProveWorkerFn: (fheKeyHex: string, crsHex: string, items: EncryptableItem[], metadata: Uint8Array) => Promise<Uint8Array>): CofheClient;
41
87
 
42
- export { areWorkersAvailable, createCofheClient, createCofheClientWithCustomWorker, createCofheConfig, createSsrStorage, hasDOM, terminateWorker };
88
+ export { areWorkersAvailable, createCofheClient, createCofheClientWithCustomWorker, createCofheConfig, createSsrStorage, getTfheThreadPoolStatus, hasDOM, terminateWorker };
package/dist/web.js CHANGED
@@ -1,7 +1,9 @@
1
- import { createCofheConfigBase, createCofheClientBase, fheTypeToString } from './chunk-43USWPEH.js';
2
- import './chunk-N6IDQRRU.js';
3
- import './chunk-Q7CBWGQX.js';
4
- import { TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT } from './chunk-ESMZCFJY.js';
1
+ import { createCofheConfigBase, createCofheClientBase } from './chunk-OL3DZN7A.js';
2
+ import './chunk-UKQAJK4N.js';
3
+ import './chunk-BQWHPIYU.js';
4
+ import { initTfheThreadPool } from './chunk-KSOV5B4B.js';
5
+ import { fheTypeToString, fromHexString } from './chunk-6ZW76ZKB.js';
6
+ import { TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT } from './chunk-XQ32JG3T.js';
5
7
  import { constructClient } from 'iframe-shared-storage';
6
8
 
7
9
  // web/const.ts
@@ -128,7 +130,7 @@ var ZkProveWorkerManager = class {
128
130
  /**
129
131
  * Submit a proof generation request to the worker
130
132
  */
131
- async submitProof(fheKeyHex, crsHex, items, metadata) {
133
+ async submitProof(fheKeyHex, crsHex, items, metadata, tfheThreads) {
132
134
  await this.initializeWorker();
133
135
  const id = `zkprove-${Date.now()}-${this.requestCounter++}`;
134
136
  return new Promise((resolve, reject) => {
@@ -143,7 +145,8 @@ var ZkProveWorkerManager = class {
143
145
  fheKeyHex,
144
146
  crsHex,
145
147
  items,
146
- metadata: Array.from(metadata)
148
+ metadata: Array.from(metadata),
149
+ tfheThreads
147
150
  };
148
151
  this.worker.postMessage(message);
149
152
  });
@@ -191,14 +194,39 @@ function areWorkersAvailable() {
191
194
  // web/index.ts
192
195
  var tfheModule = null;
193
196
  var tfheInitialized = false;
194
- async function initTfhe() {
197
+ var tfheInitPromise = null;
198
+ var threadPoolPromise = null;
199
+ var threadPoolResult = null;
200
+ function getTfheThreadPoolStatus() {
201
+ return threadPoolResult;
202
+ }
203
+ var initTfhe = async () => {
195
204
  if (tfheInitialized)
196
205
  return false;
197
- tfheModule = await import('tfhe');
198
- await tfheModule.default();
199
- await tfheModule.init_panic_hook();
200
- tfheInitialized = true;
206
+ if (tfheInitPromise) {
207
+ await tfheInitPromise;
208
+ return false;
209
+ }
210
+ tfheInitPromise = (async () => {
211
+ const mod = await import('tfhe');
212
+ await mod.default();
213
+ await mod.init_panic_hook();
214
+ tfheModule = mod;
215
+ tfheInitialized = true;
216
+ })();
217
+ try {
218
+ await tfheInitPromise;
219
+ } catch (error) {
220
+ tfheInitPromise = null;
221
+ throw error;
222
+ }
201
223
  return true;
224
+ };
225
+ function createStartMainThreadPool(tfheThreads) {
226
+ return async () => {
227
+ threadPoolPromise ??= initTfheThreadPool(requireTfhe(), tfheThreads);
228
+ threadPoolResult = await threadPoolPromise;
229
+ };
202
230
  }
203
231
  function requireTfhe() {
204
232
  if (!tfheModule) {
@@ -206,13 +234,6 @@ function requireTfhe() {
206
234
  }
207
235
  return tfheModule;
208
236
  }
209
- var fromHexString = (hexString) => {
210
- const cleanString = hexString.length % 2 === 1 ? `0${hexString}` : hexString;
211
- const arr = cleanString.replace(/^0x/, "").match(/.{1,2}/g);
212
- if (!arr)
213
- return new Uint8Array();
214
- return new Uint8Array(arr.map((byte) => parseInt(byte, 16)));
215
- };
216
237
  var _deserializeTfhePublicKey = (buff) => {
217
238
  return requireTfhe().TfheCompactPublicKey.safe_deserialize(
218
239
  fromHexString(buff),
@@ -234,13 +255,15 @@ var zkBuilderAndCrsGenerator = (fhe, crs) => {
234
255
  const zkCrs = _deserializeCompactPkeCrs(crs);
235
256
  return { zkBuilder, zkCrs };
236
257
  };
237
- async function zkProveWithWorker(fheKeyHex, crsHex, items, metadata) {
238
- const serializedItems = items.map((item) => ({
239
- utype: fheTypeToString(item.utype),
240
- data: typeof item.data === "bigint" ? item.data.toString() : item.data
241
- }));
242
- const workerManager2 = getWorkerManager();
243
- return await workerManager2.submitProof(fheKeyHex, crsHex, serializedItems, metadata);
258
+ function createZkProveWithWorker(tfheThreads) {
259
+ return async function zkProveWithWorker(fheKeyHex, crsHex, items, metadata) {
260
+ const serializedItems = items.map((item) => ({
261
+ utype: fheTypeToString(item.utype),
262
+ data: typeof item.data === "bigint" ? item.data.toString() : item.data
263
+ }));
264
+ const workerManager2 = getWorkerManager();
265
+ return await workerManager2.submitProof(fheKeyHex, crsHex, serializedItems, metadata, tfheThreads);
266
+ };
244
267
  }
245
268
  function createCofheConfig(config) {
246
269
  return createCofheConfigBase({
@@ -256,9 +279,10 @@ function createCofheClient(config) {
256
279
  tfhePublicKeyDeserializer,
257
280
  compactPkeCrsDeserializer,
258
281
  initTfhe,
282
+ beforeMainThreadProve: createStartMainThreadPool(config.tfheThreads),
259
283
  // Always provide the worker function if available - config.useWorkers controls usage
260
284
  // areWorkersAvailable will return true if the Worker API is available and false in Node.js
261
- zkProveWorkerFn: areWorkersAvailable() ? zkProveWithWorker : void 0
285
+ zkProveWorkerFn: areWorkersAvailable() ? createZkProveWithWorker(config.tfheThreads) : void 0
262
286
  });
263
287
  }
264
288
  function createCofheClientWithCustomWorker(config, customZkProveWorkerFn) {
@@ -268,8 +292,9 @@ function createCofheClientWithCustomWorker(config, customZkProveWorkerFn) {
268
292
  tfhePublicKeyDeserializer,
269
293
  compactPkeCrsDeserializer,
270
294
  initTfhe,
295
+ beforeMainThreadProve: createStartMainThreadPool(config.tfheThreads),
271
296
  zkProveWorkerFn: customZkProveWorkerFn
272
297
  });
273
298
  }
274
299
 
275
- export { areWorkersAvailable, createCofheClient, createCofheClientWithCustomWorker, createCofheConfig, createSsrStorage, hasDOM, terminateWorker };
300
+ export { areWorkersAvailable, createCofheClient, createCofheClientWithCustomWorker, createCofheConfig, createSsrStorage, getTfheThreadPoolStatus, hasDOM, terminateWorker };
@@ -1,35 +1,96 @@
1
1
  'use strict';
2
2
 
3
+ require('viem');
4
+
3
5
  // core/consts.ts
4
6
  var TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT = BigInt(1 << 30);
5
7
 
6
- // web/zkProve.worker.ts
7
- var tfheModule = null;
8
- var initialized = false;
9
- async function initTfhe() {
10
- if (initialized)
11
- return;
8
+ // web/tfheThreadPool.ts
9
+ var MAX_AUTO_THREADS = 8;
10
+ function resolveThreadCount(setting = "auto") {
11
+ if (setting === false)
12
+ return 1;
13
+ if (typeof setting === "number") {
14
+ return Number.isFinite(setting) && setting >= 1 ? Math.floor(setting) : 1;
15
+ }
16
+ const cores = globalThis.navigator?.hardwareConcurrency ?? 1;
17
+ if (!Number.isFinite(cores) || cores < 1)
18
+ return 1;
19
+ return Math.min(Math.floor(cores), MAX_AUTO_THREADS);
20
+ }
21
+ function isCrossOriginIsolated() {
22
+ return globalThis.crossOriginIsolated === true;
23
+ }
24
+ async function initTfheThreadPool(mod, setting = "auto") {
25
+ if (setting === false) {
26
+ return { enabled: false, threads: 1, reason: "disabled via config.tfheThreads" };
27
+ }
28
+ if (typeof mod.initThreadPool !== "function") {
29
+ return { enabled: false, threads: 1, reason: "tfhe build has no initThreadPool (single-threaded wasm)" };
30
+ }
31
+ if (typeof Worker === "undefined") {
32
+ return { enabled: false, threads: 1, reason: "Worker is unavailable" };
33
+ }
34
+ if (!isCrossOriginIsolated()) {
35
+ return {
36
+ enabled: false,
37
+ threads: 1,
38
+ reason: "not cross-origin isolated \u2014 serve the page with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp (or credentialless) to enable multi-threaded proving"
39
+ };
40
+ }
41
+ const threads = resolveThreadCount(setting);
42
+ if (threads <= 1) {
43
+ return { enabled: false, threads: 1, reason: "resolved to a single thread" };
44
+ }
12
45
  try {
13
- tfheModule = await import('tfhe');
14
- await tfheModule.default();
15
- await tfheModule.init_panic_hook();
16
- initialized = true;
17
- console.log("[Worker] TFHE initialized");
46
+ await mod.initThreadPool(threads);
47
+ return { enabled: true, threads };
18
48
  } catch (error) {
19
- console.error("[Worker] Failed to initialize TFHE:", error);
20
- throw error;
49
+ return {
50
+ enabled: false,
51
+ threads: 1,
52
+ reason: `initThreadPool failed: ${error instanceof Error ? error.message : String(error)}`
53
+ };
21
54
  }
22
55
  }
23
- function fromHexString(hexString) {
24
- const cleanString = hexString.length % 2 === 1 ? `0${hexString}` : hexString;
25
- const arr = cleanString.replace(/^0x/, "").match(/.{1,2}/g);
56
+ var HEX_BODY_REGEX = /^[0-9a-fA-F]*$/;
57
+ var fromHexString = (hexString) => {
58
+ const body = hexString.replace(/^0x/, "");
59
+ if (!HEX_BODY_REGEX.test(body)) {
60
+ throw new Error("Invalid hex string: contains non-hexadecimal characters");
61
+ }
62
+ const padded = body.length % 2 === 1 ? `0${body}` : body;
63
+ const arr = padded.match(/.{1,2}/g);
26
64
  if (!arr)
27
65
  return new Uint8Array();
28
66
  return new Uint8Array(arr.map((byte) => parseInt(byte, 16)));
67
+ };
68
+
69
+ // web/zkProve.worker.ts
70
+ var tfheModule = null;
71
+ var initPromise = null;
72
+ var threadPool = null;
73
+ function initTfhe(tfheThreads = "auto") {
74
+ initPromise ??= (async () => {
75
+ try {
76
+ tfheModule = await import('tfhe');
77
+ await tfheModule.default();
78
+ await tfheModule.init_panic_hook();
79
+ threadPool = await initTfheThreadPool(tfheModule, tfheThreads);
80
+ console.log(
81
+ threadPool.enabled ? `[Worker] TFHE initialized (rayon thread pool: ${threadPool.threads} threads)` : `[Worker] TFHE initialized (single-threaded: ${threadPool.reason})`
82
+ );
83
+ } catch (error) {
84
+ console.error("[Worker] Failed to initialize TFHE:", error);
85
+ initPromise = null;
86
+ throw error;
87
+ }
88
+ })();
89
+ return initPromise;
29
90
  }
30
91
  if (typeof self !== "undefined") {
31
92
  self.onmessage = async (event) => {
32
- const { id, type, fheKeyHex, crsHex, items, metadata } = event.data;
93
+ const { id, type, fheKeyHex, crsHex, items, metadata, tfheThreads } = event.data;
33
94
  if (type !== "zkProve") {
34
95
  self.postMessage({
35
96
  id,
@@ -39,7 +100,7 @@ if (typeof self !== "undefined") {
39
100
  return;
40
101
  }
41
102
  try {
42
- await initTfhe();
103
+ await initTfhe(tfheThreads);
43
104
  if (!tfheModule) {
44
105
  throw new Error("TFHE module not initialized");
45
106
  }
@@ -1,32 +1,32 @@
1
- import { TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT } from './chunk-ESMZCFJY.js';
1
+ import { initTfheThreadPool } from './chunk-KSOV5B4B.js';
2
+ import { fromHexString } from './chunk-6ZW76ZKB.js';
3
+ import { TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT } from './chunk-XQ32JG3T.js';
2
4
 
3
5
  // web/zkProve.worker.ts
4
6
  var tfheModule = null;
5
- var initialized = false;
6
- async function initTfhe() {
7
- if (initialized)
8
- return;
9
- try {
10
- tfheModule = await import('tfhe');
11
- await tfheModule.default();
12
- await tfheModule.init_panic_hook();
13
- initialized = true;
14
- console.log("[Worker] TFHE initialized");
15
- } catch (error) {
16
- console.error("[Worker] Failed to initialize TFHE:", error);
17
- throw error;
18
- }
19
- }
20
- function fromHexString(hexString) {
21
- const cleanString = hexString.length % 2 === 1 ? `0${hexString}` : hexString;
22
- const arr = cleanString.replace(/^0x/, "").match(/.{1,2}/g);
23
- if (!arr)
24
- return new Uint8Array();
25
- return new Uint8Array(arr.map((byte) => parseInt(byte, 16)));
7
+ var initPromise = null;
8
+ var threadPool = null;
9
+ function initTfhe(tfheThreads = "auto") {
10
+ initPromise ??= (async () => {
11
+ try {
12
+ tfheModule = await import('tfhe');
13
+ await tfheModule.default();
14
+ await tfheModule.init_panic_hook();
15
+ threadPool = await initTfheThreadPool(tfheModule, tfheThreads);
16
+ console.log(
17
+ threadPool.enabled ? `[Worker] TFHE initialized (rayon thread pool: ${threadPool.threads} threads)` : `[Worker] TFHE initialized (single-threaded: ${threadPool.reason})`
18
+ );
19
+ } catch (error) {
20
+ console.error("[Worker] Failed to initialize TFHE:", error);
21
+ initPromise = null;
22
+ throw error;
23
+ }
24
+ })();
25
+ return initPromise;
26
26
  }
27
27
  if (typeof self !== "undefined") {
28
28
  self.onmessage = async (event) => {
29
- const { id, type, fheKeyHex, crsHex, items, metadata } = event.data;
29
+ const { id, type, fheKeyHex, crsHex, items, metadata, tfheThreads } = event.data;
30
30
  if (type !== "zkProve") {
31
31
  self.postMessage({
32
32
  id,
@@ -36,7 +36,7 @@ if (typeof self !== "undefined") {
36
36
  return;
37
37
  }
38
38
  try {
39
- await initTfhe();
39
+ await initTfhe(tfheThreads);
40
40
  if (!tfheModule) {
41
41
  throw new Error("TFHE module not initialized");
42
42
  }
package/node/index.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  type FheKeyDeserializer,
11
11
  TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT,
12
12
  } from '@/core';
13
+ import { fromHexString } from '../core/utils.js';
13
14
 
14
15
  // Import node-specific storage (internal use only)
15
16
  import { createNodeStorage } from './storage.js';
@@ -30,16 +31,6 @@ async function initTfhe(): Promise<boolean> {
30
31
  return true;
31
32
  }
32
33
 
33
- /**
34
- * Utility to convert the hex string key to a Uint8Array for use with tfhe
35
- */
36
- const fromHexString = (hexString: string): Uint8Array => {
37
- const cleanString = hexString.length % 2 === 1 ? `0${hexString}` : hexString;
38
- const arr = cleanString.replace(/^0x/, '').match(/.{1,2}/g);
39
- if (!arr) return new Uint8Array();
40
- return new Uint8Array(arr.map((byte) => parseInt(byte, 16)));
41
- };
42
-
43
34
  const _deserializeTfhePublicKey = (buff: string): TfheCompactPublicKey => {
44
35
  return TfheCompactPublicKey.safe_deserialize(fromHexString(buff), TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT);
45
36
  };
package/node/storage.ts CHANGED
@@ -6,7 +6,18 @@ import { promises as fs } from 'fs';
6
6
  import { join } from 'path';
7
7
 
8
8
  // Memory storage fallback
9
- const memoryStorage: Record<string, string> = {};
9
+ const memoryStorage: Record<string, unknown> = {};
10
+
11
+ const getStorageDir = () => join(process.env.HOME || process.env.USERPROFILE || '.', '.cofhesdk');
12
+
13
+ /**
14
+ * Keep the directory and files private to the current user. `mode` only applies
15
+ * on creation, so chmod also tightens directories/files created by earlier SDK versions.
16
+ */
17
+ const ensureStorageDir = async (storageDir: string) => {
18
+ await fs.mkdir(storageDir, { recursive: true, mode: 0o700 });
19
+ await fs.chmod(storageDir, 0o700).catch(() => {});
20
+ };
10
21
 
11
22
  /**
12
23
  * Creates a node storage implementation using the filesystem
@@ -16,8 +27,8 @@ export const createNodeStorage = (): IStorage => {
16
27
  return {
17
28
  getItem: async (name: string) => {
18
29
  try {
19
- const storageDir = join(process.env.HOME || process.env.USERPROFILE || '.', '.cofhesdk');
20
- await fs.mkdir(storageDir, { recursive: true });
30
+ const storageDir = getStorageDir();
31
+ await ensureStorageDir(storageDir);
21
32
  const filePath = join(storageDir, `${name}.json`);
22
33
  const data = await fs.readFile(filePath, 'utf8').catch(() => null);
23
34
  return data ? JSON.parse(data) : null;
@@ -28,19 +39,19 @@ export const createNodeStorage = (): IStorage => {
28
39
  },
29
40
  setItem: async (name: string, value: any) => {
30
41
  try {
31
- const storageDir = join(process.env.HOME || process.env.USERPROFILE || '.', '.cofhesdk');
32
- await fs.mkdir(storageDir, { recursive: true });
42
+ const storageDir = getStorageDir();
43
+ await ensureStorageDir(storageDir);
33
44
  const filePath = join(storageDir, `${name}.json`);
34
- await fs.writeFile(filePath, JSON.stringify(value));
45
+ await fs.writeFile(filePath, JSON.stringify(value), { mode: 0o600 });
46
+ await fs.chmod(filePath, 0o600).catch(() => {});
35
47
  } catch (e) {
36
48
  console.warn('Node.js filesystem modules not available, falling back to memory storage' + e);
37
- memoryStorage[name] = JSON.stringify(value);
49
+ memoryStorage[name] = value;
38
50
  }
39
51
  },
40
52
  removeItem: async (name: string) => {
41
53
  try {
42
- const storageDir = join(process.env.HOME || process.env.USERPROFILE || '.', '.cofhesdk');
43
- const filePath = join(storageDir, `${name}.json`);
54
+ const filePath = join(getStorageDir(), `${name}.json`);
44
55
  await fs.unlink(filePath).catch(() => {});
45
56
  } catch (e) {
46
57
  console.warn('Node.js filesystem modules not available, falling back to memory storage' + e);
@@ -16,6 +16,10 @@ import { privateKeyToAccount } from 'viem/accounts';
16
16
  import { arbitrumSepolia, baseSepolia, sepolia } from 'viem/chains';
17
17
  import { createCofheClient, createCofheConfig } from '../index.js';
18
18
 
19
+ // Live-chain suite: needs the funded TEST_PRIVATE_KEY (transacts against pre-deployed
20
+ // testnet fixtures). Without it — fork PRs, where secrets are unavailable — the suite
21
+ // skips; CI runs it separately with the key (test-live job).
22
+ const LIVE_TESTS = Boolean(TEST_PRIVATE_KEY);
19
23
  const DEFAULT_TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
20
24
  const BOB_PRIVATE_KEY = (TEST_PRIVATE_KEY || DEFAULT_TEST_PRIVATE_KEY) as `0x${string}`;
21
25
  const ALICE_PRIVATE_KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d';
@@ -32,7 +36,7 @@ const VIEM_CHAINS: Record<number, Chain> = {
32
36
  const testViemChain = STAGING_TESTS ? stagingViemChain : arbitrumSepolia;
33
37
  const testCofheChain = STAGING_TESTS ? stagingCofhe : cofheArbSepolia;
34
38
 
35
- describe('@cofhe/node - Inherited Client Tests', () => {
39
+ describe.skipIf(!LIVE_TESTS)('@cofhe/node - Inherited Client Tests', () => {
36
40
  let cofheClient: CofheClient;
37
41
  let publicClient: PublicClient;
38
42
  let bobWalletClient: WalletClient;
@@ -0,0 +1,47 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { promises as fs } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+
6
+ import { createNodeStorage } from '../storage.js';
7
+
8
+ describe('createNodeStorage memory fallback', () => {
9
+ let originalHome: string | undefined;
10
+ let testDir: string;
11
+ let invalidHome: string;
12
+
13
+ beforeEach(async () => {
14
+ originalHome = process.env.HOME;
15
+ testDir = await fs.mkdtemp(join(tmpdir(), 'cofhesdk-storage-fallback-'));
16
+
17
+ // HOME points to a regular file, so creating HOME/.cofhesdk fails.
18
+ invalidHome = join(testDir, 'not-a-directory');
19
+ await fs.writeFile(invalidHome, 'file', 'utf8');
20
+ process.env.HOME = invalidHome;
21
+ });
22
+
23
+ afterEach(async () => {
24
+ if (originalHome === undefined) {
25
+ delete process.env.HOME;
26
+ } else {
27
+ process.env.HOME = originalHome;
28
+ }
29
+
30
+ await fs.rm(testDir, { recursive: true, force: true });
31
+ });
32
+
33
+ it('round-trips the same value when filesystem storage is unavailable', async () => {
34
+ const storage = createNodeStorage();
35
+ const value = {
36
+ state: {
37
+ fhe: { 1: { 0: 'key' } },
38
+ crs: { 1: 'crs' },
39
+ },
40
+ version: 0,
41
+ };
42
+
43
+ await storage.setItem('fallback-roundtrip', value);
44
+
45
+ await expect(storage.getItem('fallback-roundtrip')).resolves.toEqual(value);
46
+ });
47
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { promises as fs } from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { createNodeStorage } from '../storage.js';
6
+
7
+ const withTempHome = async (fn: (home: string) => Promise<void>) => {
8
+ const home = await fs.mkdtemp(path.join(os.tmpdir(), 'cofhe-home-'));
9
+ const oldHome = process.env.HOME;
10
+ process.env.HOME = home;
11
+ try {
12
+ await fn(home);
13
+ } finally {
14
+ if (oldHome === undefined) delete process.env.HOME;
15
+ else process.env.HOME = oldHome;
16
+ await fs.rm(home, { recursive: true, force: true });
17
+ }
18
+ };
19
+
20
+ describe('node storage permissions', () => {
21
+ it.skipIf(process.platform === 'win32')('does not make stored data readable by other users', () =>
22
+ withTempHome(async (home) => {
23
+ await createNodeStorage().setItem('t', { value: 1 });
24
+ const st = await fs.stat(path.join(home, '.cofhesdk', 't.json'));
25
+ expect(st.mode & 0o077).toBe(0);
26
+ })
27
+ );
28
+
29
+ it.skipIf(process.platform === 'win32')('tightens permissions of a file created with loose permissions', () =>
30
+ withTempHome(async (home) => {
31
+ const dir = path.join(home, '.cofhesdk');
32
+ await fs.mkdir(dir, { mode: 0o755 });
33
+ const file = path.join(dir, 't.json');
34
+ await fs.writeFile(file, '{}', { mode: 0o644 });
35
+ await createNodeStorage().setItem('t', { value: 1 });
36
+ expect((await fs.stat(file)).mode & 0o077).toBe(0);
37
+ expect((await fs.stat(dir)).mode & 0o077).toBe(0);
38
+ })
39
+ );
40
+ });