@cofhe/sdk 0.7.1 → 0.8.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 (79) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/acps/acp.ts +10 -1
  3. package/acps/index.ts +11 -0
  4. package/acps/onchain-utils.ts +188 -49
  5. package/acps/registry.ts +73 -0
  6. package/acps/test/onchainAccessStatus.test.ts +116 -0
  7. package/acps/types.ts +16 -0
  8. package/chains/chains/arbitrum.ts +14 -0
  9. package/chains/chains/ethereum.ts +14 -0
  10. package/chains/index.ts +5 -1
  11. package/chains/test/chains.test.ts +24 -3
  12. package/core/acps.ts +51 -143
  13. package/core/client.ts +13 -8
  14. package/core/clientTypes.ts +29 -4
  15. package/core/config.ts +25 -1
  16. package/core/consts.ts +14 -2
  17. package/core/decrypt/verifyDecryptResult.ts +4 -3
  18. package/core/encrypt/encryptInputsBuilder.ts +6 -0
  19. package/core/encrypt/zkPackProveVerify.ts +7 -1
  20. package/core/error.ts +1 -0
  21. package/core/fetchKeys.ts +2 -2
  22. package/core/index.ts +9 -1
  23. package/core/keyStore.ts +23 -10
  24. package/core/taskManager.ts +48 -0
  25. package/core/test/acps.test.ts +91 -1
  26. package/core/test/client.test.ts +41 -0
  27. package/core/test/config.test.ts +16 -0
  28. package/core/test/decrypt.test.ts +11 -4
  29. package/core/test/encryptInputsBuilder.test.ts +1 -1
  30. package/core/test/fetchKeys.test.ts +23 -2
  31. package/core/test/keyStore.test.ts +18 -6
  32. package/core/test/taskManager.test.ts +57 -0
  33. package/core/types.ts +8 -0
  34. package/dist/acps.cjs +314 -35
  35. package/dist/acps.d.cts +251 -4
  36. package/dist/acps.d.ts +251 -4
  37. package/dist/acps.js +2 -2
  38. package/dist/chains.cjs +26 -0
  39. package/dist/chains.d.cts +45 -1
  40. package/dist/chains.d.ts +45 -1
  41. package/dist/chains.js +1 -1
  42. package/dist/{chunk-43USWPEH.js → chunk-GGI7Y6AW.js} +59 -264
  43. package/dist/chunk-KSOV5B4B.js +50 -0
  44. package/dist/{chunk-ESMZCFJY.js → chunk-NMKMCAI3.js} +4 -2
  45. package/dist/{chunk-Q7CBWGQX.js → chunk-UJYOBHYA.js} +374 -37
  46. package/dist/{chunk-N6IDQRRU.js → chunk-UKQAJK4N.js} +25 -1
  47. package/dist/{clientTypes-BN3nbzYM.d.ts → clientTypes-5YPPR16m.d.ts} +77 -14
  48. package/dist/{clientTypes-CYZjFznO.d.cts → clientTypes-D3aIXmUp.d.cts} +77 -14
  49. package/dist/core.cjs +271 -136
  50. package/dist/core.d.cts +29 -8
  51. package/dist/core.d.ts +29 -8
  52. package/dist/core.js +4 -4
  53. package/dist/node.cjs +266 -136
  54. package/dist/node.d.cts +2 -2
  55. package/dist/node.d.ts +2 -2
  56. package/dist/node.js +5 -5
  57. package/dist/{acp-Wi6isVQI.d.cts → onchain-utils-CUAK8glx.d.cts} +31 -1
  58. package/dist/{acp-Wi6isVQI.d.ts → onchain-utils-CUAK8glx.d.ts} +31 -1
  59. package/dist/web.cjs +360 -150
  60. package/dist/web.d.cts +49 -3
  61. package/dist/web.d.ts +49 -3
  62. package/dist/web.js +51 -20
  63. package/dist/zkProve.worker.cjs +69 -15
  64. package/dist/zkProve.worker.js +23 -17
  65. package/node/storage.ts +2 -2
  66. package/node/test/inherited.test.ts +5 -1
  67. package/node/test/storage-memory-fallback.test.ts +47 -0
  68. package/package.json +5 -3
  69. package/web/index.ts +100 -26
  70. package/web/test/countingZkProveWorker.ts +31 -0
  71. package/web/test/inherited.web.test.ts +5 -1
  72. package/web/test/mainThreadPool.web.test.ts +174 -0
  73. package/web/test/threadPoolBenchHelper.ts +140 -0
  74. package/web/test/threadPoolOff.web.test.ts +17 -0
  75. package/web/test/threadPoolOn.web.test.ts +80 -0
  76. package/web/test/workerInitRace.web.test.ts +79 -0
  77. package/web/tfheThreadPool.ts +140 -0
  78. package/web/workerManager.ts +4 -2
  79. package/web/zkProve.worker.ts +42 -17
package/core/index.ts CHANGED
@@ -62,6 +62,9 @@ export type {
62
62
  // Util types
63
63
  EncryptStepCallbackFunction as EncryptSetStateFn,
64
64
  EncryptStepCallbackContext,
65
+ // tfhe init types
66
+ TfheInitializer,
67
+ TfheThreadsSetting,
65
68
  } from './types.js';
66
69
  export {
67
70
  FheTypes,
@@ -103,7 +106,9 @@ export { zkProveWithWorker } from './encrypt/zkPackProveVerify.js';
103
106
 
104
107
  // Consts (contract addresses, serialized size limits)
105
108
  export {
106
- TASK_MANAGER_ADDRESS,
109
+ COFHE_ADDRESS_BOOK_ADDRESS,
110
+ TASK_MANAGER_ID,
111
+ MOCKS_TASK_MANAGER_ADDRESS,
107
112
  MOCKS_ZK_VERIFIER_ADDRESS,
108
113
  MOCKS_ZK_VERIFIER_SIGNER_ADDRESS,
109
114
  MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY,
@@ -113,6 +118,9 @@ export {
113
118
  TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT,
114
119
  } from './consts.js';
115
120
 
121
+ // Task Manager resolution (CoFHEAddressBook)
122
+ export { getTaskManagerAddress } from './taskManager.js';
123
+
116
124
  // Decrypt result verification
117
125
  export { verifyDecryptResult } from './decrypt/verifyDecryptResult.js';
118
126
 
package/core/keyStore.ts CHANGED
@@ -11,15 +11,15 @@ type SecurityZoneRecord<T> = Record<number, T>;
11
11
  // Keys store for FHE keys and CRS
12
12
  export type KeysStore = {
13
13
  fhe: ChainRecord<SecurityZoneRecord<string | undefined>>;
14
- crs: ChainRecord<string | undefined>;
14
+ crs: ChainRecord<SecurityZoneRecord<string | undefined>>;
15
15
  };
16
16
 
17
17
  export type KeysStorage = {
18
18
  store: StoreApi<KeysStore>;
19
19
  getFheKey: (chainId: number | undefined, securityZone?: number) => string | undefined;
20
- getCrs: (chainId: number | undefined) => string | undefined;
20
+ getCrs: (chainId: number | undefined, securityZone?: number) => string | undefined;
21
21
  setFheKey: (chainId: number, securityZone: number, key: string) => void;
22
- setCrs: (chainId: number, crs: string) => void;
22
+ setCrs: (chainId: number, securityZone: number, crs: string) => void;
23
23
  clearKeysStorage: () => Promise<void>;
24
24
  rehydrateKeysStore: () => Promise<void>;
25
25
  };
@@ -45,6 +45,10 @@ const DEFAULT_KEYS_STORE: KeysStore = {
45
45
  crs: {},
46
46
  };
47
47
 
48
+ function isSecurityZoneRecord(value: unknown): value is SecurityZoneRecord<string | undefined> {
49
+ return value != null && typeof value === 'object';
50
+ }
51
+
48
52
  type StoreWithPersist = ReturnType<typeof createStoreWithPersit>;
49
53
 
50
54
  function isStoreWithPersist(store: StoreApi<KeysStore> | StoreWithPersist): store is StoreWithPersist {
@@ -72,9 +76,9 @@ export function createKeysStore(storage: IStorage | null): KeysStorage {
72
76
  return stored;
73
77
  };
74
78
 
75
- const getCrs = (chainId: number | undefined) => {
76
- if (chainId == null) return undefined;
77
- const stored = keysStore.getState().crs[chainId];
79
+ const getCrs = (chainId: number | undefined, securityZone = 0) => {
80
+ if (chainId == null || securityZone == null) return undefined;
81
+ const stored = keysStore.getState().crs[chainId]?.[securityZone];
78
82
  return stored;
79
83
  };
80
84
 
@@ -87,10 +91,11 @@ export function createKeysStore(storage: IStorage | null): KeysStorage {
87
91
  );
88
92
  };
89
93
 
90
- const setCrs = (chainId: number, crs: string) => {
94
+ const setCrs = (chainId: number, securityZone: number, crs: string) => {
91
95
  keysStore.setState(
92
96
  produce<KeysStore>((state: KeysStore) => {
93
- state.crs[chainId] = crs;
97
+ if (state.crs[chainId] == null) state.crs[chainId] = {};
98
+ state.crs[chainId][securityZone] = crs;
94
99
  })
95
100
  );
96
101
  };
@@ -143,8 +148,16 @@ function createStoreWithPersit(storage: IStorage) {
143
148
  mergedFhe[chainId] = { ...persistedZones, ...currentZones };
144
149
  }
145
150
 
146
- // Deep merge for crs
147
- const mergedCrs: KeysStore['crs'] = { ...persisted.crs, ...current.crs };
151
+ // Deep merge for crs. A `crs` persisted before it was keyed by security zone holds a
152
+ // bare string per chain; those entries are dropped so the CRS is refetched per zone
153
+ // rather than indexed as a string.
154
+ const mergedCrs: KeysStore['crs'] = {};
155
+ const allCrsChainIds = new Set([...Object.keys(current.crs), ...Object.keys(persisted.crs)]);
156
+ for (const chainId of allCrsChainIds) {
157
+ const persistedZones = isSecurityZoneRecord(persisted.crs[chainId]) ? persisted.crs[chainId] : {};
158
+ const currentZones = isSecurityZoneRecord(current.crs[chainId]) ? current.crs[chainId] : {};
159
+ mergedCrs[chainId] = { ...persistedZones, ...currentZones };
160
+ }
148
161
 
149
162
  return {
150
163
  fhe: mergedFhe,
@@ -0,0 +1,48 @@
1
+ import { parseAbi, type Address, type PublicClient } from 'viem';
2
+
3
+ import { COFHE_ADDRESS_BOOK_ADDRESS, TASK_MANAGER_ID } from './consts.js';
4
+ import { CofheError, CofheErrorCode } from './error.js';
5
+
6
+ const ADDRESS_BOOK_ABI = parseAbi([
7
+ 'function getTm(uint256 id) view returns (address)',
8
+ 'error TaskManagerNotSet(uint256 id)',
9
+ ]);
10
+
11
+ const taskManagerAddressCache = new Map<number, Promise<Address>>();
12
+
13
+ /** Test hook: forget resolved addresses (e.g. between redeployments on one chainId). */
14
+ export const clearTaskManagerAddressCache = () => taskManagerAddressCache.clear();
15
+
16
+ /**
17
+ * The chain's Task Manager, resolved through the CoFHEAddressBook
18
+ * (`getTm(TASK_MANAGER_ID)`) - the same lookup FHE.sol performs on every call.
19
+ *
20
+ * Resolutions are cached per chainId. A failed resolution (network error, no
21
+ * CoFHE deployment, id unset) is NOT cached, so a later call retries.
22
+ */
23
+ export const getTaskManagerAddress = async (publicClient: PublicClient, chainId?: number): Promise<Address> => {
24
+ const id = chainId ?? publicClient.chain?.id ?? (await publicClient.getChainId());
25
+
26
+ const cached = taskManagerAddressCache.get(id);
27
+ if (cached != null) return cached;
28
+
29
+ const pending = publicClient
30
+ .readContract({
31
+ address: COFHE_ADDRESS_BOOK_ADDRESS,
32
+ abi: ADDRESS_BOOK_ABI,
33
+ functionName: 'getTm',
34
+ args: [TASK_MANAGER_ID],
35
+ })
36
+ .catch((error: unknown) => {
37
+ taskManagerAddressCache.delete(id);
38
+ throw CofheError.fromError(error, {
39
+ code: CofheErrorCode.TaskManagerUnresolved,
40
+ message: `Failed to resolve the Task Manager from the CoFHEAddressBook on chain ${id}`,
41
+ hint: 'Ensure CoFHE is deployed on this chain. On a local hardhat / anvil chain, deploy the CoFHE mocks first.',
42
+ context: { chainId: id, addressBook: COFHE_ADDRESS_BOOK_ADDRESS, taskManagerId: TASK_MANAGER_ID },
43
+ });
44
+ });
45
+
46
+ taskManagerAddressCache.set(id, pending);
47
+ return pending;
48
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @vitest-environment happy-dom
3
3
  */
4
- import { acpStore } from '@/acps';
4
+ import { ACPUtils, acpStore, type IncomingShare, type SharingACP } from '@/acps';
5
5
 
6
6
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
7
 
@@ -152,6 +152,96 @@ describe('Core ACPs Tests', () => {
152
152
  });
153
153
  });
154
154
 
155
+ describe('checkAccess on the issuer copy of a share', () => {
156
+ it('is valid while unexpired and not revoked, without needing the recipient signature', async () => {
157
+ const sharing = await acps.createSharing(
158
+ { name: 'To Alice', issuer: bobAddress, recipient: aliceAddress },
159
+ publicClient,
160
+ bobWalletClient
161
+ );
162
+ expect(sharing.recipientSignature).toBe('0x');
163
+ // No revoker configured here, so nothing on chain can disable it: valid without any RPC.
164
+ expect(await acps.checkAccess(sharing, publicClient)).toBe('valid');
165
+ });
166
+
167
+ it('reports expiry locally', async () => {
168
+ const sharing = await acps.createSharing(
169
+ { name: 'Old', issuer: bobAddress, recipient: aliceAddress, expiration: 1 },
170
+ publicClient,
171
+ bobWalletClient
172
+ );
173
+ expect(await acps.checkAccess(sharing, publicClient)).toBe('expired');
174
+ });
175
+ });
176
+
177
+ describe('Import without activation', () => {
178
+ const ZERO_SHARE_ID = `0x${'0'.repeat(64)}` as const;
179
+
180
+ // Alice already decrypts with her own self acp; Bob then shares with her.
181
+ const setUp = async () => {
182
+ const self = await acps.createSelf({ name: 'Alice self', issuer: aliceAddress }, publicClient, aliceWalletClient);
183
+ const sharing = await acps.createSharing(
184
+ { name: 'Shared with Alice', issuer: bobAddress, recipient: aliceAddress },
185
+ publicClient,
186
+ bobWalletClient
187
+ );
188
+ return { self, sharing };
189
+ };
190
+
191
+ const toIncomingShare = (sharing: SharingACP): IncomingShare => {
192
+ const { sealingKey: _sealingKey, recipientSignature: _recipientSignature, ...pub } = ACPUtils.getPublic(sharing);
193
+ return { ...pub, shareId: ZERO_SHARE_ID };
194
+ };
195
+
196
+ it('importShared activates the imported acp by default', async () => {
197
+ const { sharing } = await setUp();
198
+ const imported = await acps.importShared(ACPUtils.export(sharing), publicClient, aliceWalletClient);
199
+
200
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBe(imported.hash);
201
+ });
202
+
203
+ it('importShared with activate: false stores the acp and keeps the active one', async () => {
204
+ const { self, sharing } = await setUp();
205
+ const imported = await acps.importShared(ACPUtils.export(sharing), publicClient, aliceWalletClient, {
206
+ activate: false,
207
+ });
208
+
209
+ expect(acps.getACPs(chainId, aliceAddress)?.[imported.hash]).toBeDefined();
210
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBe(self.hash);
211
+ });
212
+
213
+ it('importShared with activate: false leaves no active acp when there was none', async () => {
214
+ const sharing = await acps.createSharing(
215
+ { name: 'Shared with Alice', issuer: bobAddress, recipient: aliceAddress },
216
+ publicClient,
217
+ bobWalletClient
218
+ );
219
+ const imported = await acps.importShared(ACPUtils.export(sharing), publicClient, aliceWalletClient, {
220
+ activate: false,
221
+ });
222
+
223
+ expect(acps.getACPs(chainId, aliceAddress)?.[imported.hash]).toBeDefined();
224
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBeUndefined();
225
+ });
226
+
227
+ it('importFromChain activates the imported acp by default', async () => {
228
+ const { sharing } = await setUp();
229
+ const imported = await acps.importFromChain(toIncomingShare(sharing), publicClient, aliceWalletClient);
230
+
231
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBe(imported.hash);
232
+ });
233
+
234
+ it('importFromChain with activate: false stores the acp and keeps the active one', async () => {
235
+ const { self, sharing } = await setUp();
236
+ const imported = await acps.importFromChain(toIncomingShare(sharing), publicClient, aliceWalletClient, {
237
+ activate: false,
238
+ });
239
+
240
+ expect(acps.getACPs(chainId, aliceAddress)?.[imported.hash]).toBeDefined();
241
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBe(self.hash);
242
+ });
243
+ });
244
+
155
245
  describe('ACP Retrieval', () => {
156
246
  let createdACP: any;
157
247
  let acpHash: string;
@@ -6,6 +6,7 @@ import { CofheError, CofheErrorCode } from '../error.js';
6
6
  import { type PublicClient, type WalletClient } from 'viem';
7
7
  import { EncryptInputsBuilder } from '../encrypt/encryptInputsBuilder.js';
8
8
  import { Encryptable } from '../types.js';
9
+ import { acps } from '../acps.js';
9
10
 
10
11
  // Mock dependencies
11
12
  vi.mock('../keyStore', () => ({
@@ -428,5 +429,45 @@ describe('createCofheClientBase', () => {
428
429
  expect(client.acp).toHaveProperty('selectActiveACP');
429
430
  expect(client.acp).toHaveProperty('removeActiveACP');
430
431
  });
432
+
433
+ describe('import activation', () => {
434
+ const share = { shareId: `0x${'0'.repeat(64)}` } as any;
435
+
436
+ it('importShared forwards activate with the connected clients', async () => {
437
+ const publicClient = createMockPublicClient();
438
+ const walletClient = createMockWalletClient();
439
+ await client.connect(publicClient, walletClient);
440
+ const spy = vi.spyOn(acps, 'importShared').mockResolvedValue({} as any);
441
+
442
+ await client.acp.importShared('{}', { activate: false });
443
+
444
+ expect(spy).toHaveBeenCalledWith('{}', publicClient, walletClient, { activate: false });
445
+ spy.mockRestore();
446
+ });
447
+
448
+ it('importShared still takes explicit clients, activating by default', async () => {
449
+ await client.connect(createMockPublicClient(), createMockWalletClient());
450
+ const publicClient = createMockPublicClient();
451
+ const walletClient = createMockWalletClient();
452
+ const spy = vi.spyOn(acps, 'importShared').mockResolvedValue({} as any);
453
+
454
+ await client.acp.importShared('{}', { publicClient, walletClient });
455
+
456
+ expect(spy).toHaveBeenCalledWith('{}', publicClient, walletClient, { activate: undefined });
457
+ spy.mockRestore();
458
+ });
459
+
460
+ it('importFromChain forwards activate', async () => {
461
+ const publicClient = createMockPublicClient();
462
+ const walletClient = createMockWalletClient();
463
+ await client.connect(publicClient, walletClient);
464
+ const spy = vi.spyOn(acps, 'importFromChain').mockResolvedValue({} as any);
465
+
466
+ await client.acp.importFromChain(share, { activate: false });
467
+
468
+ expect(spy).toHaveBeenCalledWith(share, publicClient, walletClient, { activate: false });
469
+ spy.mockRestore();
470
+ });
471
+ });
431
472
  });
432
473
  });
@@ -162,6 +162,22 @@ describe('createCofheConfigBase', () => {
162
162
  expectValidConfigItem('useWorkers', undefined, true); // defaults to true
163
163
  });
164
164
 
165
+ it('tfheThreads', () => {
166
+ expectInvalidConfigItem('tfheThreads', 'not-a-setting');
167
+ expectInvalidConfigItem('tfheThreads', null);
168
+ expectInvalidConfigItem('tfheThreads', true);
169
+ expectInvalidConfigItem('tfheThreads', 0);
170
+ expectInvalidConfigItem('tfheThreads', -4);
171
+ expectInvalidConfigItem('tfheThreads', 2.5);
172
+ expectInvalidConfigItem('tfheThreads', {});
173
+
174
+ expectValidConfigItem('tfheThreads', 'auto', 'auto');
175
+ expectValidConfigItem('tfheThreads', false, false);
176
+ expectValidConfigItem('tfheThreads', 1, 1);
177
+ expectValidConfigItem('tfheThreads', 4, 4);
178
+ expectValidConfigItem('tfheThreads', undefined, 'auto'); // defaults to 'auto'
179
+ });
180
+
165
181
  it('should get config item', () => {
166
182
  const config: CofheInputConfig = {
167
183
  supportedChains: [sepolia],
@@ -1,5 +1,5 @@
1
1
  import { STAGING_TESTS, stagingViemChain } from './stagingRedirect';
2
- import { FheTypes, verifyDecryptResult, createCofheConfigBase, TASK_MANAGER_ADDRESS } from '@/core';
2
+ import { FheTypes, verifyDecryptResult, createCofheConfigBase, getTaskManagerAddress } from '@/core';
3
3
  import { getChainById, stagingCofhe } from '@/chains';
4
4
  import {
5
5
  TEST_PRIVATE_KEY,
@@ -19,7 +19,12 @@ import { createPublicClient, createWalletClient, http, parseAbi } from 'viem';
19
19
  import { privateKeyToAccount } from 'viem/accounts';
20
20
  import { arbitrumSepolia, baseSepolia, sepolia } from 'viem/chains';
21
21
 
22
- const account = privateKeyToAccount(TEST_PRIVATE_KEY);
22
+ // Live-chain suite: needs the funded TEST_PRIVATE_KEY (signs decrypt requests against
23
+ // pre-deployed testnet fixtures). Without it — fork PRs, where secrets are unavailable —
24
+ // the suite skips; CI runs it separately with the key (test-live job).
25
+ const LIVE_TESTS = Boolean(TEST_PRIVATE_KEY);
26
+ const DEFAULT_TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
27
+ const account = privateKeyToAccount(TEST_PRIVATE_KEY || DEFAULT_TEST_PRIVATE_KEY);
23
28
 
24
29
  const TEST_CHAIN_ID = STAGING_TESTS ? stagingViemChain.id : PRIMARY_TEST_CHAIN;
25
30
 
@@ -29,7 +34,7 @@ const VIEM_CHAINS: Record<number, Chain> = {
29
34
  11155111: sepolia,
30
35
  };
31
36
 
32
- describe('Core – Decrypt Tests', () => {
37
+ describe.skipIf(!LIVE_TESTS)('Core – Decrypt Tests', () => {
33
38
  let publicClient: PublicClient;
34
39
  let walletClient: WalletClient;
35
40
  let config: ReturnType<typeof createCofheConfigBase>;
@@ -236,6 +241,8 @@ describe('Core – Decrypt Tests', () => {
236
241
  },
237
242
  ] as const;
238
243
 
244
+ const taskManagerAddress = await getTaskManagerAddress(publicClient);
245
+
239
246
  for (const sample of samples) {
240
247
  const sdkResult = await verifyDecryptResult(sample.handle, sample.cleartext, sample.signature, publicClient);
241
248
 
@@ -243,7 +250,7 @@ describe('Core – Decrypt Tests', () => {
243
250
  'function verifyDecryptResultSafe(uint256 ctHash, uint256 cleartext, bytes signature) view returns (bool)',
244
251
  ]);
245
252
  const tmResult = await publicClient.readContract({
246
- address: TASK_MANAGER_ADDRESS,
253
+ address: taskManagerAddress,
247
254
  abi: verifyDecryptResultSafeAbi,
248
255
  functionName: 'verifyDecryptResultSafe',
249
256
  args: [sample.handle, sample.cleartext, sample.signature],
@@ -180,7 +180,7 @@ let keysStorage: KeysStorage;
180
180
 
181
181
  const insertMockKeys = (chainId: number, securityZone: number) => {
182
182
  keysStorage.setFheKey(chainId, securityZone, '0x1234567890');
183
- keysStorage.setCrs(chainId, '0x1234567890');
183
+ keysStorage.setCrs(chainId, securityZone, '0x1234567890');
184
184
  };
185
185
 
186
186
  const mockTfhePublicKeyDeserializer: FheKeyDeserializer = (buff: string) => {
@@ -110,7 +110,7 @@ describe('fetchKeys', () => {
110
110
  it('should not fetch CRS if already cached', async () => {
111
111
  // Pre-populate with a cached CRS
112
112
  const mockCachedCrs = '0x2345678901';
113
- keysStorage.setCrs(sepolia.id, mockCachedCrs);
113
+ keysStorage.setCrs(sepolia.id, 0, mockCachedCrs);
114
114
 
115
115
  const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
116
116
  config,
@@ -133,12 +133,33 @@ describe('fetchKeys', () => {
133
133
  expect(retrievedKey).toBeDefined();
134
134
  });
135
135
 
136
+ it('should not reuse a cached CRS from a different security zone', async () => {
137
+ // CoFHE serves a distinct CRS per security zone, so a zone 0 CRS must not satisfy zone 1
138
+ const zone0Crs = '0x2345678901';
139
+ keysStorage.setCrs(sepolia.id, 0, zone0Crs);
140
+
141
+ const [, [crs, crsFetchedFromCoFHE]] = await fetchKeys(
142
+ config,
143
+ sepolia.id,
144
+ 1,
145
+ mockTfhePublicKeyDeserializer,
146
+ mockCompactPkeCrsDeserializer,
147
+ keysStorage
148
+ );
149
+
150
+ expect(crsFetchedFromCoFHE).toBe(true);
151
+ expect(crs).not.toEqual(zone0Crs);
152
+
153
+ // The zone 0 entry is untouched
154
+ expect(keysStorage.getCrs(sepolia.id, 0)).toEqual(zone0Crs);
155
+ });
156
+
136
157
  it('should not make any network calls if both keys are cached', async () => {
137
158
  // Pre-populate both keys
138
159
  const mockCachedKey = '0x1234567890';
139
160
  const mockCachedCrs = '0x2345678901';
140
161
  keysStorage.setFheKey(sepolia.id, 0, mockCachedKey);
141
- keysStorage.setCrs(sepolia.id, mockCachedCrs);
162
+ keysStorage.setCrs(sepolia.id, 0, mockCachedCrs);
142
163
 
143
164
  const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
144
165
  config,
@@ -98,7 +98,7 @@ describe('KeyStore', () => {
98
98
  const testCrs = '0x1234567890';
99
99
 
100
100
  it('should set and get CRS', () => {
101
- keysStorage.setCrs(testChainId, testCrs);
101
+ keysStorage.setCrs(testChainId, 0, testCrs);
102
102
 
103
103
  const retrievedCrs = keysStorage.getCrs(testChainId);
104
104
 
@@ -109,16 +109,28 @@ describe('KeyStore', () => {
109
109
  const crs1 = '0x1234567890';
110
110
  const crs2 = '0x2345678901';
111
111
 
112
- keysStorage.setCrs(1, crs1);
113
- keysStorage.setCrs(2, crs2);
112
+ keysStorage.setCrs(1, 0, crs1);
113
+ keysStorage.setCrs(2, 0, crs2);
114
114
 
115
115
  expect(keysStorage.getCrs(1)).toEqual(crs1);
116
116
  expect(keysStorage.getCrs(2)).toEqual(crs2);
117
117
  });
118
118
 
119
+ it('should keep CRS separate per security zone', () => {
120
+ const crsZone0 = '0xaaaaaaaaaa';
121
+ const crsZone1 = '0xbbbbbbbbbb';
122
+
123
+ keysStorage.setCrs(testChainId, 0, crsZone0);
124
+ keysStorage.setCrs(testChainId, 1, crsZone1);
125
+
126
+ expect(keysStorage.getCrs(testChainId, 0)).toEqual(crsZone0);
127
+ expect(keysStorage.getCrs(testChainId, 1)).toEqual(crsZone1);
128
+ });
129
+
119
130
  it('should return undefined for non-existent CRS', () => {
120
131
  expect(keysStorage.getCrs(999)).toBeUndefined();
121
132
  expect(keysStorage.getCrs(undefined)).toBeUndefined();
133
+ expect(keysStorage.getCrs(testChainId, 999)).toBeUndefined();
122
134
  });
123
135
  });
124
136
 
@@ -162,7 +174,7 @@ describe('KeyStore', () => {
162
174
  const testCrs = '0x2345678901';
163
175
 
164
176
  keysStorage.setFheKey(testChainId, 0, testKey);
165
- keysStorage.setCrs(testChainId, testCrs);
177
+ keysStorage.setCrs(testChainId, 0, testCrs);
166
178
 
167
179
  expect(keysStorage.getFheKey(testChainId, 0)).toEqual(testKey);
168
180
  expect(keysStorage.getCrs(testChainId)).toEqual(testCrs);
@@ -194,13 +206,13 @@ describe('KeyStore', () => {
194
206
 
195
207
  keysStorage.setFheKey(1, 0, key1);
196
208
  keysStorage.setFheKey(2, 0, key2);
197
- keysStorage.setCrs(1, crs1);
209
+ keysStorage.setCrs(1, 0, crs1);
198
210
 
199
211
  const state = keysStorage.store.getState();
200
212
 
201
213
  expect(state.fhe[1][0]).toEqual(key1);
202
214
  expect(state.fhe[2][0]).toEqual(key2);
203
- expect(state.crs[1]).toEqual(crs1);
215
+ expect(state.crs[1][0]).toEqual(crs1);
204
216
  });
205
217
  });
206
218
 
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import type { PublicClient } from 'viem';
3
+
4
+ import { getTaskManagerAddress, clearTaskManagerAddressCache } from '../taskManager.js';
5
+ import { COFHE_ADDRESS_BOOK_ADDRESS, TASK_MANAGER_ID } from '../consts.js';
6
+ import { CofheErrorCode, isCofheError } from '../error.js';
7
+
8
+ const TM = '0x0000000000000000000000000000000000005000';
9
+
10
+ const fakeClient = (readContract: (...args: any[]) => Promise<unknown>, chainId = 31337) =>
11
+ ({ chain: { id: chainId }, readContract: vi.fn(readContract) }) as unknown as PublicClient & {
12
+ readContract: ReturnType<typeof vi.fn>;
13
+ };
14
+
15
+ describe('getTaskManagerAddress', () => {
16
+ beforeEach(() => clearTaskManagerAddressCache());
17
+
18
+ it('resolves getTm(TASK_MANAGER_ID) on the CoFHEAddressBook', async () => {
19
+ const client = fakeClient(async () => TM);
20
+
21
+ expect(await getTaskManagerAddress(client)).to.equal(TM);
22
+ expect(client.readContract).toHaveBeenCalledWith(
23
+ expect.objectContaining({
24
+ address: COFHE_ADDRESS_BOOK_ADDRESS,
25
+ functionName: 'getTm',
26
+ args: [TASK_MANAGER_ID],
27
+ })
28
+ );
29
+ });
30
+
31
+ it('caches per chainId and shares an in-flight lookup', async () => {
32
+ const client = fakeClient(async () => TM);
33
+
34
+ await Promise.all([getTaskManagerAddress(client), getTaskManagerAddress(client)]);
35
+ await getTaskManagerAddress(client);
36
+ expect(client.readContract).toHaveBeenCalledTimes(1);
37
+
38
+ await getTaskManagerAddress(client, 11155111);
39
+ expect(client.readContract).toHaveBeenCalledTimes(2);
40
+ });
41
+
42
+ it('throws TASK_MANAGER_UNRESOLVED and does not cache the failure', async () => {
43
+ let calls = 0;
44
+ const client = fakeClient(async () => {
45
+ calls += 1;
46
+ if (calls === 1) throw new Error('returned no data ("0x")');
47
+ return TM;
48
+ });
49
+
50
+ const error = await getTaskManagerAddress(client).catch((e: unknown) => e);
51
+ expect(isCofheError(error)).to.equal(true);
52
+ expect((error as any).code).to.equal(CofheErrorCode.TaskManagerUnresolved);
53
+
54
+ expect(await getTaskManagerAddress(client)).to.equal(TM);
55
+ expect(client.readContract).toHaveBeenCalledTimes(2);
56
+ });
57
+ });
package/core/types.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  export type TfheInitializer = () => Promise<boolean>;
2
2
 
3
+ /**
4
+ * How many threads tfhe's rayon pool may use for ZK proof generation (web only).
5
+ * - `'auto'` (default): derive from `navigator.hardwareConcurrency`, capped
6
+ * - a positive number: request exactly that many
7
+ * - `false`: stay single-threaded
8
+ */
9
+ export type TfheThreadsSetting = number | 'auto' | false;
10
+
3
11
  export interface IStorage {
4
12
  getItem: (name: string) => Promise<any>;
5
13
  setItem: (name: string, value: any) => Promise<void>;