@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
@@ -1,12 +1,24 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { sepolia, arbSepolia, baseSepolia, hardhat, chains, getChainById, getChainByName } from '../index.js';
2
+ import {
3
+ sepolia,
4
+ arbSepolia,
5
+ baseSepolia,
6
+ ethereum,
7
+ arbitrum,
8
+ hardhat,
9
+ chains,
10
+ getChainById,
11
+ getChainByName,
12
+ } from '../index.js';
3
13
 
4
14
  describe('Chains', () => {
5
15
  it('should export all chains', () => {
6
- expect(Object.keys(chains)).toHaveLength(6);
16
+ expect(Object.keys(chains)).toHaveLength(8);
7
17
  expect(chains).toHaveProperty('sepolia');
8
18
  expect(chains).toHaveProperty('arbSepolia');
9
19
  expect(chains).toHaveProperty('baseSepolia');
20
+ expect(chains).toHaveProperty('ethereum');
21
+ expect(chains).toHaveProperty('arbitrum');
10
22
  expect(chains).toHaveProperty('hardhat');
11
23
  expect(chains).toHaveProperty('localcofhe');
12
24
  expect(chains).toHaveProperty('stagingCofhe');
@@ -17,6 +29,11 @@ describe('Chains', () => {
17
29
  expect(sepolia.name).toBe('Sepolia');
18
30
  expect(sepolia.environment).toBe('TESTNET');
19
31
 
32
+ expect(ethereum.id).toBe(1);
33
+ expect(ethereum.environment).toBe('MAINNET');
34
+ expect(arbitrum.id).toBe(42161);
35
+ expect(arbitrum.environment).toBe('MAINNET');
36
+
20
37
  expect(hardhat.id).toBe(31337);
21
38
  expect(hardhat.name).toBe('Hardhat');
22
39
  expect(hardhat.environment).toBe('MOCK');
@@ -24,6 +41,8 @@ describe('Chains', () => {
24
41
 
25
42
  it('should find chains by ID', () => {
26
43
  expect(getChainById(11155111)).toBe(sepolia);
44
+ expect(getChainById(1)).toBe(ethereum);
45
+ expect(getChainById(42161)).toBe(arbitrum);
27
46
  expect(getChainById(31337)).toBe(hardhat);
28
47
  expect(getChainById(999999)).toBeUndefined();
29
48
  });
@@ -31,12 +50,14 @@ describe('Chains', () => {
31
50
  it('should find chains by name', () => {
32
51
  expect(getChainByName('sepolia')).toBe(sepolia);
33
52
  expect(getChainByName('Sepolia')).toBe(sepolia);
53
+ expect(getChainByName('ethereum')).toBe(ethereum);
54
+ expect(getChainByName('arbitrum')).toBe(arbitrum);
34
55
  expect(getChainByName('hardhat')).toBe(hardhat);
35
56
  expect(getChainByName('nonexistent')).toBeUndefined();
36
57
  });
37
58
 
38
59
  it('should validate chain properties', () => {
39
- const allChains = [sepolia, arbSepolia, baseSepolia, hardhat];
60
+ const allChains = [sepolia, arbSepolia, baseSepolia, ethereum, arbitrum, hardhat];
40
61
 
41
62
  allChains.forEach((chain) => {
42
63
  expect(typeof chain.id).toBe('number');
package/core/acps.ts CHANGED
@@ -11,26 +11,17 @@ import {
11
11
  type IncomingShare,
12
12
  type SharingACP,
13
13
  type ACPHashFields,
14
+ type ACPAccessStatus,
15
+ ACP_REVOKER_ABI,
16
+ ACP_SHARE_REGISTRY_ABI,
17
+ toChainShare,
18
+ computeShareId,
19
+ shareIdOfChainShare,
20
+ getAclServedAddresses,
21
+ clearAclServedAddresses,
14
22
  } from '@/acps';
15
23
 
16
- import {
17
- type Hex,
18
- type PublicClient,
19
- type WalletClient,
20
- encodeAbiParameters,
21
- keccak256,
22
- parseAbi,
23
- zeroAddress,
24
- } from 'viem';
25
-
26
- import { TASK_MANAGER_ADDRESS } from './consts.js';
27
-
28
- // ACP default revoker (timestamp-based revocation) — interface shared by all revokers
29
- const ACP_VALIDATOR_ABI = parseAbi([
30
- 'function revokeSingle(uint256 id)',
31
- 'function revokeAllExisting()',
32
- 'function disabled(address issuer, uint256 id) view returns (bool)',
33
- ]);
24
+ import { type Hex, type PublicClient, type WalletClient, zeroAddress } from 'viem';
34
25
 
35
26
  // HELPERS
36
27
 
@@ -51,9 +42,10 @@ const storeActiveACP = async (acp: ACP, publicClient: any, walletClient: any) =>
51
42
  };
52
43
 
53
44
  // Generic function to handle acp creation with error handling.
54
- // `activate` controls whether the new acp becomes the issuer's active acp — true for
55
- // self/imported acps (the connected user decrypts with them), false for sharing acps (those
56
- // are delegated to a recipient and are never the issuer's own active acp).
45
+ // `activate` controls whether the new acp becomes the connected account's active acp — true for
46
+ // self acps and, by default, imported acps (the connected user decrypts with them); false for
47
+ // sharing acps (delegated to a recipient, never the issuer's own active acp) and for imports
48
+ // made with `activate: false`.
57
49
  const createACPWithSign = async <T, TACP extends ACP>(
58
50
  options: T,
59
51
  publicClient: PublicClient,
@@ -96,12 +88,23 @@ const createSharing = async (
96
88
  return createACPWithSign(options, publicClient, walletClient, ACPUtils.createSharingAndSign, false);
97
89
  };
98
90
 
91
+ /** Options for importing a shared acp. */
92
+ export type ImportActivationOptions = {
93
+ /**
94
+ * Make the imported acp the active one. Defaults to `true`.
95
+ * With `false` the acp is only stored: decrypts that use the active acp keep using the current one,
96
+ * and the imported acp is used only where it is passed explicitly (e.g. `.withACP(acp)`).
97
+ */
98
+ activate?: boolean;
99
+ };
100
+
99
101
  const importShared = async (
100
102
  options: ImportSharedACPOptions | string,
101
103
  publicClient: PublicClient,
102
- walletClient: WalletClient
104
+ walletClient: WalletClient,
105
+ { activate = true }: ImportActivationOptions = {}
103
106
  ): Promise<RecipientACP> => {
104
- return createACPWithSign(options, publicClient, walletClient, ACPUtils.importSharedAndSign);
107
+ return createACPWithSign(options, publicClient, walletClient, ACPUtils.importSharedAndSign, activate);
105
108
  };
106
109
 
107
110
  // ACP UTILS
@@ -263,65 +266,6 @@ const applyACPDefaults = <
263
266
  return result;
264
267
  };
265
268
 
266
- // ACL-SERVED ADDRESSES (defaultRevokerContract / shareRegistry)
267
-
268
- const ACL_SERVED_ADDRESSES_ABI = parseAbi([
269
- 'function acl() view returns (address)',
270
- 'function defaultRevokerContract() view returns (address)',
271
- 'function shareRegistry() view returns (address)',
272
- ]);
273
-
274
- export interface AclServedAddresses {
275
- defaultRevoker?: Hex;
276
- shareRegistry?: Hex;
277
- }
278
-
279
- const aclServedAddressesCache = new Map<number, AclServedAddresses>();
280
-
281
- /** Test hook: forget resolved addresses (e.g. between redeployments on one chainId). */
282
- const clearAclServedAddresses = () => aclServedAddressesCache.clear();
283
-
284
- /**
285
- * The ACP infrastructure addresses the chain's ACL serves (TaskManager -> acl()
286
- * -> getters). Zero addresses and pre-upgrade ACLs (getters absent -> revert)
287
- * resolve to `undefined` — callers fall back to `acp.*` config.
288
- *
289
- * Resolutions are cached per chainId. A failure to reach the TaskManager (network
290
- * error, no CoFHE deployment) is NOT cached, so a transient outage does not pin
291
- * an empty result for the whole session.
292
- */
293
- const getAclServedAddresses = async (publicClient: PublicClient, chainId: number): Promise<AclServedAddresses> => {
294
- const cached = aclServedAddressesCache.get(chainId);
295
- if (cached != null) return cached;
296
-
297
- let aclAddress: Hex;
298
- try {
299
- aclAddress = await publicClient.readContract({
300
- address: TASK_MANAGER_ADDRESS,
301
- abi: ACL_SERVED_ADDRESSES_ABI,
302
- functionName: 'acl',
303
- });
304
- } catch {
305
- return {};
306
- }
307
-
308
- const [defaultRevoker, shareRegistry] = await Promise.all([
309
- publicClient
310
- .readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName: 'defaultRevokerContract' })
311
- .catch(() => undefined),
312
- publicClient
313
- .readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName: 'shareRegistry' })
314
- .catch(() => undefined),
315
- ]);
316
-
317
- const resolved: AclServedAddresses = {
318
- defaultRevoker: defaultRevoker != null && defaultRevoker !== zeroAddress ? defaultRevoker : undefined,
319
- shareRegistry: shareRegistry != null && shareRegistry !== zeroAddress ? shareRegistry : undefined,
320
- };
321
- aclServedAddressesCache.set(chainId, resolved);
322
- return resolved;
323
- };
324
-
325
269
  /**
326
270
  * `applyACPDefaults` with the ACL consulted for the default revoker when
327
271
  * `acp.defaultRevoker` config does not name one for this chain — explicit
@@ -374,7 +318,7 @@ const revokeACP = async (acp: ACP, walletClient: WalletClient): Promise<Hex> =>
374
318
 
375
319
  return walletClient.writeContract({
376
320
  address: acp.revokerContract,
377
- abi: ACP_VALIDATOR_ABI,
321
+ abi: ACP_REVOKER_ABI,
378
322
  functionName: 'revokeSingle',
379
323
  args: [BigInt(acp.revokerData)],
380
324
  account: walletClient.account,
@@ -410,7 +354,7 @@ const revokeAllACPs = async (
410
354
 
411
355
  return walletClient.writeContract({
412
356
  address: revoker,
413
- abi: ACP_VALIDATOR_ABI,
357
+ abi: ACP_REVOKER_ABI,
414
358
  functionName: 'revokeAllExisting',
415
359
  args: [],
416
360
  account: walletClient.account,
@@ -418,6 +362,20 @@ const revokeAllACPs = async (
418
362
  });
419
363
  };
420
364
 
365
+ /**
366
+ * The ACP on-chain status as a value instead of a revert (see `ACPAccessStatus`): validity
367
+ * without a handle, and whether the ACP may read `handle` with one.
368
+ */
369
+ const checkAccess = async (acp: ACP, publicClient: PublicClient, handle?: bigint | Hex): Promise<ACPAccessStatus> => {
370
+ // The issuer half of a share carries no recipient signature (the recipient adds it on import), so
371
+ // the ACL check would always fail it. What can change for the issuer is expiry and revocation.
372
+ if (acp.type === 'sharing' && handle === undefined) {
373
+ if (ACPUtils.isExpired(acp)) return 'expired';
374
+ return (await isACPRevoked(acp, publicClient)) ? 'revoked' : 'valid';
375
+ }
376
+ return ACPUtils.checkAccessOnChain(acp, publicClient, handle);
377
+ };
378
+
421
379
  /**
422
380
  * Check whether an ACP has been revoked (or is otherwise disabled) by its
423
381
  * revoker. Returns false for acps without a revoker (not revocable).
@@ -426,65 +384,12 @@ const isACPRevoked = async (acp: ACP, publicClient: PublicClient): Promise<boole
426
384
  if (acp.revokerContract === zeroAddress || acp.revokerData === 0) return false;
427
385
  return publicClient.readContract({
428
386
  address: acp.revokerContract,
429
- abi: ACP_VALIDATOR_ABI,
387
+ abi: ACP_REVOKER_ABI,
430
388
  functionName: 'disabled',
431
389
  args: [acp.issuer, BigInt(acp.revokerData)],
432
390
  });
433
391
  };
434
392
 
435
- // SHARE (on-chain, via the ACPShareRegistry)
436
-
437
- const ACP_SHARE_REGISTRY_ABI = parseAbi([
438
- 'struct ACP { address issuer; uint64 expiration; address recipient; uint256 revokerData; address revokerContract; uint8 scope; address[] contracts; bytes32[] handles; bytes32 sealingKey; bytes issuerSignature; bytes recipientSignature; }',
439
- 'function share(ACP calldata acp) external returns (bytes32)',
440
- 'function removeShare(bytes32 shareId) external',
441
- 'function sharesFor(address recipient) external view returns (ACP[] memory)',
442
- 'function getShare(bytes32 shareId) external view returns (ACP memory)',
443
- 'function isShareValid(bytes32 shareId) external view returns (bool)',
444
- ]);
445
-
446
- const ACP_TUPLE = [
447
- {
448
- type: 'tuple',
449
- components: [
450
- { name: 'issuer', type: 'address' },
451
- { name: 'expiration', type: 'uint64' },
452
- { name: 'recipient', type: 'address' },
453
- { name: 'revokerData', type: 'uint256' },
454
- { name: 'revokerContract', type: 'address' },
455
- { name: 'scope', type: 'uint8' },
456
- { name: 'contracts', type: 'address[]' },
457
- { name: 'handles', type: 'bytes32[]' },
458
- { name: 'sealingKey', type: 'bytes32' },
459
- { name: 'issuerSignature', type: 'bytes' },
460
- { name: 'recipientSignature', type: 'bytes' },
461
- ],
462
- },
463
- ] as const;
464
-
465
- const ZERO_BYTES32 = `0x${'0'.repeat(64)}` as Hex;
466
-
467
- /** The on-chain payload for a sharing ACP: recipient-side fields empty. */
468
- const toChainShare = (acp: ACP) => {
469
- // Same public struct as the off-chain export flow (ACPUtils.getPublic), with
470
- // the recipient-side fields blanked — the recipient supplies them at import —
471
- // and uint fields widened for the ABI encoder.
472
- const pub = ACPUtils.getPublic(acp, true);
473
- return {
474
- ...pub,
475
- expiration: BigInt(pub.expiration),
476
- revokerData: BigInt(pub.revokerData),
477
- sealingKey: ZERO_BYTES32,
478
- recipientSignature: '0x' as Hex,
479
- };
480
- };
481
-
482
- /** Mirrors the registry's `keccak256(abi.encode(acp))` share id. */
483
- const computeShareId = (acp: ACP): Hex => {
484
- const p = toChainShare(acp);
485
- return keccak256(encodeAbiParameters(ACP_TUPLE, [p]));
486
- };
487
-
488
393
  /**
489
394
  * Post a signed sharing ACP to the on-chain share registry for its recipient
490
395
  * to discover and import — the on-chain alternative to `export()`.
@@ -531,7 +436,7 @@ const getIncomingShares = async (
531
436
  });
532
437
 
533
438
  return raw.map((s) => ({
534
- shareId: keccak256(encodeAbiParameters(ACP_TUPLE, [s])),
439
+ shareId: shareIdOfChainShare(s),
535
440
  issuer: s.issuer,
536
441
  expiration: Number(s.expiration),
537
442
  recipient: s.recipient,
@@ -546,16 +451,18 @@ const getIncomingShares = async (
546
451
 
547
452
  /**
548
453
  * Import a share read from the registry: fills the recipient's sealing key,
549
- * signs, stores and activates — the on-chain counterpart of importing an
550
- * exported JSON blob. The share stays on-chain until dismissed.
454
+ * signs, stores and (unless `activate: false`) activates — the on-chain
455
+ * counterpart of importing an exported JSON blob. The share stays on-chain
456
+ * until dismissed.
551
457
  */
552
458
  const importFromChain = async (
553
459
  share: IncomingShare,
554
460
  publicClient: PublicClient,
555
- walletClient: WalletClient
461
+ walletClient: WalletClient,
462
+ importOptions: ImportActivationOptions = {}
556
463
  ): Promise<RecipientACP> => {
557
464
  const { shareId: _shareId, ...options } = share;
558
- return importShared({ ...options, type: 'sharing' }, publicClient, walletClient);
465
+ return importShared({ ...options, type: 'sharing' }, publicClient, walletClient, importOptions);
559
466
  };
560
467
 
561
468
  /** Remove a share from the registry (issuer retracts / recipient dismisses). */
@@ -608,6 +515,7 @@ export const acps = {
608
515
  revokeACP,
609
516
  revokeAllACPs,
610
517
  isACPRevoked,
518
+ checkAccess,
611
519
 
612
520
  shareOnChain,
613
521
  getIncomingShares,
package/core/client.ts CHANGED
@@ -156,6 +156,7 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
156
156
  zkBuilderAndCrsGenerator: opts.zkBuilderAndCrsGenerator,
157
157
  initTfhe: opts.initTfhe,
158
158
  zkProveWorkerFn: opts.zkProveWorkerFn,
159
+ beforeMainThreadProve: opts.beforeMainThreadProve,
159
160
 
160
161
  keysStorage,
161
162
 
@@ -259,13 +260,11 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
259
260
  );
260
261
  },
261
262
 
262
- importShared: async (
263
- options: ImportSharedACPOptions | string,
264
- clients?: { publicClient: PublicClient; walletClient: WalletClient }
265
- ) => {
263
+ importShared: async (options: ImportSharedACPOptions | string, importOptions = {}) => {
266
264
  _requireConnected();
267
- const { publicClient, walletClient } = clients ?? connectStore.getState();
268
- return acps.importShared(options, publicClient!, walletClient!);
265
+ const { activate, ...clients } = importOptions;
266
+ const { publicClient, walletClient } = clients.publicClient ? clients : connectStore.getState();
267
+ return acps.importShared(options, publicClient!, walletClient!, { activate });
269
268
  },
270
269
 
271
270
  // Get or create methods (require connection)
@@ -314,6 +313,12 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
314
313
  return acps.isACPRevoked(acp, publicClient!);
315
314
  },
316
315
 
316
+ checkAccess: async (acp, handle) => {
317
+ _requireConnected();
318
+ const { publicClient } = connectStore.getState();
319
+ return acps.checkAccess(acp, publicClient!, handle);
320
+ },
321
+
317
322
  // On-chain sharing (require connection + config.acp.sharingRegistry)
318
323
  shareOnChain: async (acp) => {
319
324
  _requireConnected();
@@ -332,10 +337,10 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
332
337
  return acps.getIncomingShares(publicClient!, sharingRegistry, account);
333
338
  },
334
339
 
335
- importFromChain: async (share) => {
340
+ importFromChain: async (share, options) => {
336
341
  _requireConnected();
337
342
  const { publicClient, walletClient } = connectStore.getState();
338
- return acps.importFromChain(share, publicClient!, walletClient!);
343
+ return acps.importFromChain(share, publicClient!, walletClient!, options);
339
344
  },
340
345
 
341
346
  dismissShare: async (shareId) => {
@@ -6,10 +6,11 @@ import { type DecryptForTxBuilderUnset } from './decrypt/decryptForTxBuilder.js'
6
6
  import { type EncryptInputsBuilderUnset } from './encrypt/encryptInputsBuilder.js';
7
7
  import { type ZkBuilderAndCrsGenerator, type ZkProveWorkerFunction } from './encrypt/zkPackProveVerify.js';
8
8
  import { type FheKeyDeserializer } from './fetchKeys.js';
9
- import { acps } from './acps.js';
9
+ import { acps, type ImportActivationOptions } from './acps.js';
10
10
  import type { EncryptableItem, FheTypes, TfheInitializer } from './types.js';
11
11
  import type { ACPUtils } from 'acps/acp.js';
12
12
  import type {
13
+ ACPAccessStatus,
13
14
  CreateSelfACPOptions,
14
15
  ACP,
15
16
  CreateSharingACPOptions,
@@ -74,6 +75,13 @@ export type CofheClientACPsClients = {
74
75
  walletClient: WalletClient;
75
76
  };
76
77
 
78
+ /**
79
+ * Options for `importShared`. `activate: false` stores the imported acp without making it the active
80
+ * one (default `true`). Clients default to the connected ones; pass both to override.
81
+ */
82
+ export type CofheClientImportSharedOptions = ImportActivationOptions &
83
+ (CofheClientACPsClients | { publicClient?: undefined; walletClient?: undefined });
84
+
77
85
  export type CofheClientACPs = {
78
86
  getSnapshot: typeof acps.getSnapshot;
79
87
  subscribe: typeof acps.subscribe;
@@ -81,7 +89,10 @@ export type CofheClientACPs = {
81
89
  // Creation methods (require connection, no params)
82
90
  createSelf: (options: CreateSelfACPOptions, clients?: CofheClientACPsClients) => Promise<SelfACP>;
83
91
  createSharing: (options: CreateSharingACPOptions, clients?: CofheClientACPsClients) => Promise<SharingACP>;
84
- importShared: (options: ImportSharedACPOptions | string, clients?: CofheClientACPsClients) => Promise<RecipientACP>;
92
+ importShared: (
93
+ options: ImportSharedACPOptions | string,
94
+ importOptions?: CofheClientImportSharedOptions
95
+ ) => Promise<RecipientACP>;
85
96
 
86
97
  // Retrieval methods (chainId/account optional)
87
98
  getACP: (hash: string, chainId?: number, account?: string) => ACP | undefined;
@@ -102,13 +113,20 @@ export type CofheClientACPs = {
102
113
  revokeACP: (acp: ACP) => Promise<`0x${string}`>;
103
114
  revokeAllACPs: (revokerContract?: `0x${string}`) => Promise<`0x${string}`>;
104
115
  isACPRevoked: (acp: ACP) => Promise<boolean>;
116
+ /**
117
+ * The ACP on-chain status as a value instead of a revert. Without a handle: 'valid', or
118
+ * 'expired' / 'revoked' / an invalid signature. With a handle, a valid ACP reports
119
+ * 'allowed', 'out-of-scope' or 'issuer-not-allowed'. For the issuer copy of a share (type
120
+ * 'sharing', no recipient signature yet) the handle-less check covers expiry and revocation only.
121
+ */
122
+ checkAccess: (acp: ACP, handle?: bigint | `0x${string}`) => Promise<ACPAccessStatus>;
105
123
 
106
124
  /** Post a signed sharing ACP to the on-chain share registry (ACL-served, or config `acp.sharingRegistry`). */
107
125
  shareOnChain: (acp: ACP) => Promise<{ txHash: `0x${string}`; shareId: `0x${string}` }>;
108
126
  /** Importable shares addressed to the connected account (unexpired, not revoked). */
109
127
  getIncomingShares: () => Promise<IncomingShare[]>;
110
- /** Import a share read from the registry: sign as recipient, store and activate. */
111
- importFromChain: (share: IncomingShare) => Promise<RecipientACP>;
128
+ /** Import a share read from the registry: sign as recipient, store and (unless `activate: false`) activate. */
129
+ importFromChain: (share: IncomingShare, options?: ImportActivationOptions) => Promise<RecipientACP>;
112
130
  /** Recipient-side: remove a share from the registry (after import, or to decline). */
113
131
  dismissShare: (shareId: `0x${string}`) => Promise<`0x${string}`>;
114
132
  /** Issuer-side: retract a pending share from the registry. */
@@ -128,4 +146,11 @@ export type CofheClientParams<TConfig extends CofheConfig> = {
128
146
  compactPkeCrsDeserializer: FheKeyDeserializer;
129
147
  initTfhe: TfheInitializer;
130
148
  zkProveWorkerFn?: ZkProveWorkerFunction;
149
+ /**
150
+ * Runs right before a ZK proof is generated on the calling thread, i.e. only
151
+ * when the worker path is disabled, unavailable or has failed. Platforms use
152
+ * it for setup that only pays off when that thread does the proving (web
153
+ * starts tfhe's rayon thread pool here).
154
+ */
155
+ beforeMainThreadProve?: () => Promise<void>;
131
156
  };
package/core/config.ts CHANGED
@@ -3,7 +3,7 @@ import { type CofheChain } from '@/chains';
3
3
  import { z } from 'zod';
4
4
  import { type WalletClient } from 'viem';
5
5
  import { CofheError, CofheErrorCode } from './error.js';
6
- import { type IStorage } from './types.js';
6
+ import { type IStorage, type TfheThreadsSetting } from './types.js';
7
7
 
8
8
  export type CofheEnvironment = 'node' | 'hardhat' | 'web' | 'react';
9
9
 
@@ -29,6 +29,25 @@ export type CofheConfig = {
29
29
  * Default: true
30
30
  */
31
31
  useWorkers: boolean;
32
+ /**
33
+ * How many threads tfhe's rayon pool may use for ZK proof generation
34
+ * (web platform only; ignored on node/hardhat).
35
+ *
36
+ * Proving dominates the cost of `encryptInputs`, and tfhe's wasm is built with
37
+ * rayon, so spreading it across threads is a large win — roughly 5x on an
38
+ * 8-thread pool.
39
+ *
40
+ * Requires a cross-origin-isolated page (`Cross-Origin-Opener-Policy: same-origin`
41
+ * plus `Cross-Origin-Embedder-Policy: require-corp` or `credentialless`). When the
42
+ * page isn't isolated the pool is skipped and proving stays single-threaded, so
43
+ * this is safe to leave on.
44
+ *
45
+ * `'auto'` derives the count from `navigator.hardwareConcurrency` (capped, since
46
+ * the speedup flattens out); a number requests exactly that many; `false` keeps
47
+ * tfhe single-threaded.
48
+ * Default: 'auto'
49
+ */
50
+ tfheThreads: TfheThreadsSetting;
32
51
  /** ACP acp defaults, applied when creating acps (user options always win) */
33
52
  acp: {
34
53
  /**
@@ -108,6 +127,11 @@ export const CofheConfigSchema = z.object({
108
127
  .default(null),
109
128
  /** Whether to use Web Workers for ZK proof generation (web platform only) */
110
129
  useWorkers: z.boolean().optional().default(true),
130
+ /** How many threads tfhe's rayon pool may use for ZK proof generation (web platform only) */
131
+ tfheThreads: z
132
+ .union([z.number().int().positive(), z.literal('auto'), z.literal(false)])
133
+ .optional()
134
+ .default('auto'),
111
135
  /** ACP acp defaults */
112
136
  acp: z
113
137
  .object({
package/core/consts.ts CHANGED
@@ -1,5 +1,17 @@
1
- /** Main Task Manager contract address */
2
- export const TASK_MANAGER_ADDRESS = '0xeA30c4B8b44078Bbf8a6ef5b9f1eC1626C7848D9' as const;
1
+ /**
2
+ * CoFHEAddressBook contract address - the same on every chain. Resolves `TASK_MANAGER_ID` to the
3
+ * chain's Task Manager. Keep in sync with `COFHE_ADDRESS_BOOK` in `@fhenixprotocol/cofhe-contracts/FHE.sol`.
4
+ */
5
+ export const COFHE_ADDRESS_BOOK_ADDRESS = '0xC0F4e00E531a2B086492Ae3DCC1515038307196b' as const;
6
+
7
+ /** Id of the Task Manager in the CoFHEAddressBook. Keep in sync with `TASK_MANAGER_ID` in FHE.sol */
8
+ export const TASK_MANAGER_ID = 1n;
9
+
10
+ /**
11
+ * Mock Task Manager contract address (used for testing). Registered in the mock CoFHEAddressBook;
12
+ * SDK reads still resolve the Task Manager through the book.
13
+ */
14
+ export const MOCKS_TASK_MANAGER_ADDRESS = '0x0000000000000000000000000000000000005000' as const;
3
15
 
4
16
  /** Mock ZK Verifier contract address (used for testing) */
5
17
  export const MOCKS_ZK_VERIFIER_ADDRESS = '0x0000000000000000000000000000000000005001' as const;
@@ -9,7 +9,7 @@ import {
9
9
  type Hex,
10
10
  type PublicClient,
11
11
  } from 'viem';
12
- import { TASK_MANAGER_ADDRESS } from '../consts.js';
12
+ import { getTaskManagerAddress } from '../taskManager.js';
13
13
 
14
14
  const decryptResultSignerAbi = parseAbi(['function decryptResultSigner() view returns (address)']);
15
15
  const UINT_TYPE_MASK = 0x7fn;
@@ -33,7 +33,8 @@ const buildDecryptResultHash = (ctHash: bigint, cleartext: bigint, chainId: numb
33
33
  * This mirrors the TaskManager decrypt-result hash format:
34
34
  * `keccak256(abi.encodePacked(result, encType, chainId, ctHash))`
35
35
  *
36
- * The only on-chain read performed is `TaskManager.decryptResultSigner()` (via `eth_call`).
36
+ * The only on-chain reads performed are the Task Manager lookup in the CoFHEAddressBook (cached
37
+ * per chain) and `TaskManager.decryptResultSigner()` (via `eth_call`).
37
38
  *
38
39
  * Works with both production and mock deployments.
39
40
  */
@@ -45,7 +46,7 @@ export async function verifyDecryptResult(
45
46
  ): Promise<boolean> {
46
47
  const chainId = publicClient.chain?.id ?? (await publicClient.getChainId());
47
48
  const expectedSigner = await publicClient.readContract({
48
- address: TASK_MANAGER_ADDRESS,
49
+ address: await getTaskManagerAddress(publicClient, chainId),
49
50
  abi: decryptResultSignerAbi,
50
51
  functionName: 'decryptResultSigner',
51
52
  args: [],
@@ -37,6 +37,7 @@ type EncryptInputsBuilderParams<T extends EncryptableItem[]> = BaseBuilderParams
37
37
  zkBuilderAndCrsGenerator: ZkBuilderAndCrsGenerator | undefined;
38
38
  initTfhe: TfheInitializer | undefined;
39
39
  zkProveWorkerFn: ZkProveWorkerFunction | undefined;
40
+ beforeMainThreadProve?: (() => Promise<void>) | undefined;
40
41
 
41
42
  keysStorage: KeysStorage | undefined;
42
43
  };
@@ -69,6 +70,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
69
70
  private zkBuilderAndCrsGenerator: ZkBuilderAndCrsGenerator;
70
71
  private initTfhe: TfheInitializer | undefined;
71
72
  private zkProveWorkerFn: ZkProveWorkerFunction | undefined;
73
+ private beforeMainThreadProve: (() => Promise<void>) | undefined;
72
74
 
73
75
  private keysStorage: KeysStorage | undefined;
74
76
 
@@ -143,6 +145,9 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
143
145
  // Optional zkProve worker function, will be used on web if useWorkers is true and worker function is provided
144
146
  this.zkProveWorkerFn = params.zkProveWorkerFn;
145
147
 
148
+ // Optional platform setup that only matters when this thread generates the proof
149
+ this.beforeMainThreadProve = params.beforeMainThreadProve;
150
+
146
151
  // Keys storage is used to store the FHE key and CRS
147
152
  this.keysStorage = params.keysStorage;
148
153
 
@@ -582,6 +587,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
582
587
 
583
588
  if (proof == null) {
584
589
  // Use main thread directly (workers disabled or unavailable)
590
+ await this.beforeMainThreadProve?.();
585
591
  proof = await zkProve(zkBuilder, zkCrs, metadata);
586
592
  usedWorker = false;
587
593
  }
@@ -1,6 +1,6 @@
1
1
  import { TFHE_RS_ZK_MAX_BITS, TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT } from '../consts';
2
2
  import { CofheError, CofheErrorCode } from '../error';
3
- import { type EncryptableItem, FheTypes } from '../types';
3
+ import { type EncryptableItem, FheTypes, type TfheThreadsSetting } from '../types';
4
4
  import { toBigIntOrThrow, validateBigIntInRange, toHexString, hexToBytes } from '../utils';
5
5
 
6
6
  // ===== TYPES =====
@@ -29,6 +29,12 @@ export interface ZkProveWorkerRequest {
29
29
  data: any;
30
30
  }>;
31
31
  metadata: number[]; // Uint8Array serialized as array
32
+ /**
33
+ * Forwarded from `config.tfheThreads`. The worker applies it the first time it
34
+ * initialises tfhe — `initThreadPool` can only run once per wasm instance, so
35
+ * later requests carrying a different value have no effect.
36
+ */
37
+ tfheThreads?: TfheThreadsSetting;
32
38
  }
33
39
 
34
40
  /**
package/core/error.ts CHANGED
@@ -43,6 +43,7 @@ export enum CofheErrorCode {
43
43
  PublicWalletGetChainIdFailed = 'PUBLIC_WALLET_GET_CHAIN_ID_FAILED',
44
44
  PublicWalletGetAddressesFailed = 'PUBLIC_WALLET_GET_ADDRESSES_FAILED',
45
45
  RehydrateKeysStoreFailed = 'REHYDRATE_KEYS_STORE_FAILED',
46
+ TaskManagerUnresolved = 'TASK_MANAGER_UNRESOLVED',
46
47
 
47
48
  // Threshold-network stable error codes (decrypt/sealoutput), see API-RESPONSES.md
48
49
  BadRequest = 'BAD_REQUEST',
package/core/fetchKeys.ts CHANGED
@@ -77,7 +77,7 @@ const fetchCrs = async (
77
77
  keysStorage?: KeysStorage | null
78
78
  ): Promise<[string, boolean]> => {
79
79
  // Escape if key already exists
80
- const storedKey = keysStorage?.getCrs(chainId);
80
+ const storedKey = keysStorage?.getCrs(chainId, securityZone);
81
81
  const [storedKeyValid] = checkKeyValidity(storedKey, compactPkeCrsDeserializer);
82
82
  if (storedKeyValid) return [storedKey!, false];
83
83
 
@@ -108,7 +108,7 @@ const fetchCrs = async (
108
108
  throw new Error(`Error serializing CRS; ${err}`);
109
109
  }
110
110
 
111
- keysStorage?.setCrs(chainId, crs_data);
111
+ keysStorage?.setCrs(chainId, securityZone, crs_data);
112
112
 
113
113
  return [crs_data, true];
114
114
  };