@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
@@ -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 ImportSharedOptions, type ShareOnChainOptions } 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,
@@ -18,7 +19,9 @@ import type {
18
19
  RecipientACP,
19
20
  SelfACP,
20
21
  IncomingShare,
22
+ LabelledShare,
21
23
  } from 'acps/types.js';
24
+ import type { ShareLabelCheck, ShareLabelVerifyMode } from 'acps/share-metadata/verify.js';
22
25
 
23
26
  // CLIENT
24
27
 
@@ -74,6 +77,14 @@ export type CofheClientACPsClients = {
74
77
  walletClient: WalletClient;
75
78
  };
76
79
 
80
+ /**
81
+ * Options for `importShared`. `activate: false` stores the imported acp without making it the active
82
+ * one (default `true`). `name` names it (default "Shared by 0x1234…abcd"). Clients default to the
83
+ * connected ones; pass both to override.
84
+ */
85
+ export type CofheClientImportSharedOptions = ImportSharedOptions &
86
+ (CofheClientACPsClients | { publicClient?: undefined; walletClient?: undefined });
87
+
77
88
  export type CofheClientACPs = {
78
89
  getSnapshot: typeof acps.getSnapshot;
79
90
  subscribe: typeof acps.subscribe;
@@ -81,7 +92,10 @@ export type CofheClientACPs = {
81
92
  // Creation methods (require connection, no params)
82
93
  createSelf: (options: CreateSelfACPOptions, clients?: CofheClientACPsClients) => Promise<SelfACP>;
83
94
  createSharing: (options: CreateSharingACPOptions, clients?: CofheClientACPsClients) => Promise<SharingACP>;
84
- importShared: (options: ImportSharedACPOptions | string, clients?: CofheClientACPsClients) => Promise<RecipientACP>;
95
+ importShared: (
96
+ options: ImportSharedACPOptions | string,
97
+ importOptions?: CofheClientImportSharedOptions
98
+ ) => Promise<RecipientACP>;
85
99
 
86
100
  // Retrieval methods (chainId/account optional)
87
101
  getACP: (hash: string, chainId?: number, account?: string) => ACP | undefined;
@@ -102,13 +116,35 @@ export type CofheClientACPs = {
102
116
  revokeACP: (acp: ACP) => Promise<`0x${string}`>;
103
117
  revokeAllACPs: (revokerContract?: `0x${string}`) => Promise<`0x${string}`>;
104
118
  isACPRevoked: (acp: ACP) => Promise<boolean>;
119
+ /**
120
+ * The ACP on-chain status as a value instead of a revert. Without a handle: 'valid', or
121
+ * 'expired' / 'revoked' / an invalid signature. With a handle, a valid ACP reports
122
+ * 'allowed', 'out-of-scope' or 'issuer-not-allowed'. For the issuer copy of a share (type
123
+ * 'sharing', no recipient signature yet) the handle-less check covers expiry and revocation only.
124
+ */
125
+ checkAccess: (acp: ACP, handle?: bigint | `0x${string}`) => Promise<ACPAccessStatus>;
105
126
 
106
- /** Post a signed sharing ACP to the on-chain share registry (ACL-served, or config `acp.sharingRegistry`). */
107
- shareOnChain: (acp: ACP) => Promise<{ txHash: `0x${string}`; shareId: `0x${string}` }>;
108
- /** Importable shares addressed to the connected account (unexpired, not revoked). */
127
+ /**
128
+ * Post a signed sharing ACP to the on-chain share registry (ACL-served, or config `acp.sharingRegistry`),
129
+ * optionally with the labels of its handles (`options.labels`, one per handle) or an encoded blob.
130
+ */
131
+ shareOnChain: (acp: ACP, options?: ShareOnChainOptions) => Promise<{ txHash: `0x${string}`; shareId: `0x${string}` }>;
132
+ /** Importable shares addressed to the connected account (unexpired, not revoked), with their metadata. */
109
133
  getIncomingShares: () => Promise<IncomingShare[]>;
110
- /** Import a share read from the registry: sign as recipient, store and activate. */
111
- importFromChain: (share: IncomingShare) => Promise<RecipientACP>;
134
+ /** One share by id, as posted; null when unknown or removed. */
135
+ getShareFromChain: (shareId: `0x${string}`) => Promise<IncomingShare | null>;
136
+ /**
137
+ * Check a share's labels against the chain, index for index with its handles, as far as
138
+ * `verify` says (default 'all'). Takes a share read from the registry or an acp imported from
139
+ * exported JSON (or the issuer's sharing acp). Null for a share without metadata. Throws on a
140
+ * malformed blob.
141
+ */
142
+ verifyShareLabels: (
143
+ share: LabelledShare,
144
+ options?: { verify?: ShareLabelVerifyMode }
145
+ ) => Promise<ShareLabelCheck[] | null>;
146
+ /** Import a share read from the registry: sign as recipient, store and (unless `activate: false`) activate. */
147
+ importFromChain: (share: IncomingShare, options?: ImportSharedOptions) => Promise<RecipientACP>;
112
148
  /** Recipient-side: remove a share from the registry (after import, or to decline). */
113
149
  dismissShare: (shareId: `0x${string}`) => Promise<`0x${string}`>;
114
150
  /** Issuer-side: retract a pending share from the registry. */
@@ -128,4 +164,11 @@ export type CofheClientParams<TConfig extends CofheConfig> = {
128
164
  compactPkeCrsDeserializer: FheKeyDeserializer;
129
165
  initTfhe: TfheInitializer;
130
166
  zkProveWorkerFn?: ZkProveWorkerFunction;
167
+ /**
168
+ * Runs right before a ZK proof is generated on the calling thread, i.e. only
169
+ * when the worker path is disabled, unavailable or has failed. Platforms use
170
+ * it for setup that only pays off when that thread does the proving (web
171
+ * starts tfhe's rayon thread pool here).
172
+ */
173
+ beforeMainThreadProve?: () => Promise<void>;
131
174
  };
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
  /**
@@ -54,6 +73,13 @@ export type CofheConfig = {
54
73
  * names a registry.
55
74
  */
56
75
  sharingRegistry?: Record<number, `0x${string}`>;
76
+ /**
77
+ * The name of this app, e.g. 'explorer.fhenix.io'. Set, every share the client creates
78
+ * carries the app's key (`shareAppKey(app)`, kept in the share metadata), and
79
+ * `getIncomingShares` lists only the shares posted for this app. Unset, shares carry no
80
+ * key and every importable share is listed.
81
+ */
82
+ app?: string;
57
83
  };
58
84
  /** Mocks configs */
59
85
  mocks: {
@@ -108,12 +134,18 @@ export const CofheConfigSchema = z.object({
108
134
  .default(null),
109
135
  /** Whether to use Web Workers for ZK proof generation (web platform only) */
110
136
  useWorkers: z.boolean().optional().default(true),
137
+ /** How many threads tfhe's rayon pool may use for ZK proof generation (web platform only) */
138
+ tfheThreads: z
139
+ .union([z.number().int().positive(), z.literal('auto'), z.literal(false)])
140
+ .optional()
141
+ .default('auto'),
111
142
  /** ACP acp defaults */
112
143
  acp: z
113
144
  .object({
114
145
  defaultRevoker: z.custom<Record<number, `0x${string}`>>().optional(),
115
146
  defaultContractScopes: z.custom<Record<number, `0x${string}`[]>>().optional(),
116
147
  sharingRegistry: z.custom<Record<number, `0x${string}`>>().optional(),
148
+ app: z.string().min(1).optional(),
117
149
  })
118
150
  .optional()
119
151
  .default({}),
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;
@@ -48,7 +48,10 @@ const BACKEND_ERROR_CODE_TO_COFHE_ERROR_CODE: Record<BackendApiErrorCode, CofheE
48
48
  };
49
49
 
50
50
  function isBackendApiErrorCode(value: string): value is BackendApiErrorCode {
51
- return value in BACKEND_ERROR_CODE_TO_COFHE_ERROR_CODE;
51
+ // Own-property check, not `in`: `in` also matches Object.prototype keys, so codes like
52
+ // `toString` or `constructor` would be treated as recognised and the lookup would return a
53
+ // function instead of a CofheErrorCode.
54
+ return Object.prototype.hasOwnProperty.call(BACKEND_ERROR_CODE_TO_COFHE_ERROR_CODE, value);
52
55
  }
53
56
 
54
57
  export type ParsedApiError = {
@@ -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;
@@ -30,10 +30,14 @@ const buildDecryptResultHash = (ctHash: bigint, cleartext: bigint, chainId: numb
30
30
  *
31
31
  * The recovered signer must equal the on-chain configured `decryptResultSigner`.
32
32
  *
33
+ * Fails closed: if the chain has no `decryptResultSigner` set (zero address), this returns `false`.
34
+ * The on-chain `verifyDecryptResult` instead accepts any signature in that case.
35
+ *
33
36
  * This mirrors the TaskManager decrypt-result hash format:
34
37
  * `keccak256(abi.encodePacked(result, encType, chainId, ctHash))`
35
38
  *
36
- * The only on-chain read performed is `TaskManager.decryptResultSigner()` (via `eth_call`).
39
+ * The only on-chain reads performed are the Task Manager lookup in the CoFHEAddressBook (cached
40
+ * per chain) and `TaskManager.decryptResultSigner()` (via `eth_call`).
37
41
  *
38
42
  * Works with both production and mock deployments.
39
43
  */
@@ -45,13 +49,13 @@ export async function verifyDecryptResult(
45
49
  ): Promise<boolean> {
46
50
  const chainId = publicClient.chain?.id ?? (await publicClient.getChainId());
47
51
  const expectedSigner = await publicClient.readContract({
48
- address: TASK_MANAGER_ADDRESS,
52
+ address: await getTaskManagerAddress(publicClient, chainId),
49
53
  abi: decryptResultSignerAbi,
50
54
  functionName: 'decryptResultSigner',
51
55
  args: [],
52
56
  });
53
57
 
54
- if (isAddressEqual(expectedSigner, zeroAddress)) return true;
58
+ if (isAddressEqual(expectedSigner, zeroAddress)) return false;
55
59
 
56
60
  const ctHash = BigInt(handle);
57
61
  const messageHash = buildDecryptResultHash(ctHash, cleartext, chainId);
@@ -1,5 +1,14 @@
1
1
  import { type EncryptableItem, FheTypes } from '../types.js';
2
- import { type VerifyBatchResult } from './zkPackProveVerify.js';
2
+ import {
3
+ type VerifyBatchResult,
4
+ MAX_UINT8,
5
+ MAX_UINT16,
6
+ MAX_UINT32,
7
+ MAX_UINT64,
8
+ MAX_UINT128,
9
+ MAX_UINT160,
10
+ } from './zkPackProveVerify.js';
11
+ import { toBigIntOrThrow, validateBigIntInRange } from '../utils.js';
3
12
  import { createWalletClient, http, encodePacked, keccak256, type PublicClient, type WalletClient } from 'viem';
4
13
  import { MockZkVerifierAbi } from './MockZkVerifierAbi.js';
5
14
  import { hardhat } from 'viem/chains';
@@ -19,7 +28,9 @@ function createMockZkVerifierSigner() {
19
28
  });
20
29
  }
21
30
  /**
22
- * The mocks don't use a tfhe builder, so we check the encryptable bits here to preserve parity
31
+ * The mocks don't use a tfhe builder, so we check the encryptable bits and value ranges here to
32
+ * preserve parity with `zkPack`. Without the range check, MockCoFHE masks an out-of-range value
33
+ * with the utype mask (e.g. uint8(256) -> 0) instead of rejecting it like the production path.
23
34
  */
24
35
  export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]): Promise<void> {
25
36
  let totalBits = 0;
@@ -30,22 +41,27 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
30
41
  break;
31
42
  }
32
43
  case FheTypes.Uint8: {
44
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT8);
33
45
  totalBits += 8;
34
46
  break;
35
47
  }
36
48
  case FheTypes.Uint16: {
49
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT16);
37
50
  totalBits += 16;
38
51
  break;
39
52
  }
40
53
  case FheTypes.Uint32: {
54
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT32);
41
55
  totalBits += 32;
42
56
  break;
43
57
  }
44
58
  case FheTypes.Uint64: {
59
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT64);
45
60
  totalBits += 64;
46
61
  break;
47
62
  }
48
63
  case FheTypes.Uint128: {
64
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT128);
49
65
  totalBits += 128;
50
66
  break;
51
67
  }
@@ -55,6 +71,7 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
55
71
  // break;
56
72
  // }
57
73
  case FheTypes.Uint160: {
74
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT160);
58
75
  totalBits += 160;
59
76
  break;
60
77
  }
@@ -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
  };
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
+ };