@cofhe/sdk 0.7.0 → 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.
- package/CHANGELOG.md +37 -0
- package/acps/acp.ts +10 -1
- package/acps/index.ts +11 -0
- package/acps/onchain-utils.ts +188 -49
- package/acps/registry.ts +73 -0
- package/acps/test/onchainAccessStatus.test.ts +116 -0
- package/acps/types.ts +16 -0
- package/chains/chains/arbitrum.ts +14 -0
- package/chains/chains/ethereum.ts +14 -0
- package/chains/index.ts +5 -1
- package/chains/test/chains.test.ts +24 -3
- package/core/acps.ts +51 -143
- package/core/client.ts +13 -8
- package/core/clientTypes.ts +29 -4
- package/core/config.ts +25 -1
- package/core/consts.ts +14 -2
- package/core/decrypt/verifyDecryptResult.ts +4 -3
- package/core/encrypt/encryptInputsBuilder.ts +6 -0
- package/core/encrypt/zkPackProveVerify.ts +7 -1
- package/core/error.ts +1 -0
- package/core/fetchKeys.ts +2 -2
- package/core/index.ts +9 -1
- package/core/keyStore.ts +23 -10
- package/core/taskManager.ts +48 -0
- package/core/test/acps.test.ts +91 -1
- package/core/test/client.test.ts +41 -0
- package/core/test/config.test.ts +16 -0
- package/core/test/decrypt.test.ts +11 -4
- package/core/test/encryptInputsBuilder.test.ts +1 -1
- package/core/test/fetchKeys.test.ts +23 -2
- package/core/test/keyStore.test.ts +18 -6
- package/core/test/taskManager.test.ts +57 -0
- package/core/types.ts +8 -0
- package/dist/acps.cjs +314 -35
- package/dist/acps.d.cts +251 -4
- package/dist/acps.d.ts +251 -4
- package/dist/acps.js +2 -2
- package/dist/chains.cjs +26 -0
- package/dist/chains.d.cts +45 -1
- package/dist/chains.d.ts +45 -1
- package/dist/chains.js +1 -1
- package/dist/{chunk-43USWPEH.js → chunk-GGI7Y6AW.js} +59 -264
- package/dist/chunk-KSOV5B4B.js +50 -0
- package/dist/{chunk-ESMZCFJY.js → chunk-NMKMCAI3.js} +4 -2
- package/dist/{chunk-Q7CBWGQX.js → chunk-UJYOBHYA.js} +374 -37
- package/dist/{chunk-N6IDQRRU.js → chunk-UKQAJK4N.js} +25 -1
- package/dist/{clientTypes-BN3nbzYM.d.ts → clientTypes-5YPPR16m.d.ts} +77 -14
- package/dist/{clientTypes-CYZjFznO.d.cts → clientTypes-D3aIXmUp.d.cts} +77 -14
- package/dist/core.cjs +271 -136
- package/dist/core.d.cts +29 -8
- package/dist/core.d.ts +29 -8
- package/dist/core.js +4 -4
- package/dist/node.cjs +266 -136
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +5 -5
- package/dist/{acp-Wi6isVQI.d.cts → onchain-utils-CUAK8glx.d.cts} +31 -1
- package/dist/{acp-Wi6isVQI.d.ts → onchain-utils-CUAK8glx.d.ts} +31 -1
- package/dist/web.cjs +360 -150
- package/dist/web.d.cts +49 -3
- package/dist/web.d.ts +49 -3
- package/dist/web.js +51 -20
- package/dist/zkProve.worker.cjs +69 -15
- package/dist/zkProve.worker.js +23 -17
- package/node/storage.ts +2 -2
- package/node/test/inherited.test.ts +5 -1
- package/node/test/storage-memory-fallback.test.ts +47 -0
- package/package.json +5 -3
- package/web/index.ts +100 -26
- package/web/test/countingZkProveWorker.ts +31 -0
- package/web/test/inherited.web.test.ts +5 -1
- package/web/test/mainThreadPool.web.test.ts +174 -0
- package/web/test/threadPoolBenchHelper.ts +140 -0
- package/web/test/threadPoolOff.web.test.ts +17 -0
- package/web/test/threadPoolOn.web.test.ts +80 -0
- package/web/test/workerInitRace.web.test.ts +79 -0
- package/web/tfheThreadPool.ts +140 -0
- package/web/workerManager.ts +4 -2
- 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
|
-
|
|
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] =
|
|
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
|
-
|
|
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
|
+
};
|
package/core/test/acps.test.ts
CHANGED
|
@@ -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;
|
package/core/test/client.test.ts
CHANGED
|
@@ -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
|
});
|
package/core/test/config.test.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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:
|
|
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>;
|