@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
@@ -1,13 +1,26 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { type PublicClient, type WalletClient, createPublicClient, createWalletClient, http } from 'viem';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { type PublicClient, type WalletClient, createPublicClient, createWalletClient, http, zeroAddress } from 'viem';
3
3
  import { privateKeyToAccount } from 'viem/accounts';
4
4
  import { arbitrumSepolia } from 'viem/chains';
5
+ import { ACPUtils, acpStore, type ACP } from '@/acps';
5
6
  import { DecryptForTxBuilder } from '../decrypt/decryptForTxBuilder.js';
6
7
  import { DecryptForViewBuilder } from '../decrypt/decryptForViewBuilder.js';
8
+ import { tnDecryptV2 } from '../decrypt/tnDecryptV2';
9
+ import { tnSealOutputV2 } from '../decrypt/tnSealOutputV2';
7
10
  import { createCofheConfigBase, type CofheConfig } from '../config.js';
8
11
  import { CofheErrorCode } from '../error.js';
9
12
  import { FheTypes } from '../types.js';
10
13
 
14
+ // Stub the threshold network calls so tests can assert whether a request was dispatched
15
+ vi.mock('../decrypt/tnDecryptV2', async (importOriginal) => ({
16
+ ...(await importOriginal<typeof import('../decrypt/tnDecryptV2')>()),
17
+ tnDecryptV2: vi.fn(),
18
+ }));
19
+ vi.mock('../decrypt/tnSealOutputV2', async (importOriginal) => ({
20
+ ...(await importOriginal<typeof import('../decrypt/tnSealOutputV2')>()),
21
+ tnSealOutputV2: vi.fn(),
22
+ }));
23
+
11
24
  const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
12
25
  const account = privateKeyToAccount(TEST_PRIVATE_KEY);
13
26
  const TEST_CHAIN_ID = 421614;
@@ -416,3 +429,98 @@ describe('DecryptForViewBuilder', () => {
416
429
  });
417
430
  });
418
431
  });
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // Expired acp (regression, issue #272)
435
+ //
436
+ // The getOrCreate* half of #272 is covered in acps.test.ts. These cover the decrypt path:
437
+ // execute() must reject an expired acp (ACPUtils.validate) before any request is dispatched
438
+ // to the threshold network, however the acp was selected.
439
+ // ---------------------------------------------------------------------------
440
+
441
+ describe('execute – expired acp (regression, issue #272)', () => {
442
+ type Selection = 'active acp' | 'acp hash' | 'acp object';
443
+ const selections: Selection[] = ['active acp', 'acp hash', 'acp object'];
444
+
445
+ const now = () => Math.floor(Date.now() / 1000);
446
+
447
+ // Built by hand rather than with ACPUtils.sign, which reads the ACL's EIP-712 domain over RPC.
448
+ // validate() only checks that a signature is present, and the threshold network is stubbed.
449
+ function createSignedSelfACP(expiration: number): ACP {
450
+ return {
451
+ ...ACPUtils.createSelf({ name: 'Test Self ACP', issuer: account.address, expiration }),
452
+ issuerSignature: `0x${'11'.repeat(65)}`,
453
+ _signedDomain: { name: 'ACL', version: '1', chainId: TEST_CHAIN_ID, verifyingContract: zeroAddress },
454
+ };
455
+ }
456
+
457
+ // Stores the acp and makes it the active acp for the test chain + account
458
+ function seedACP(acp: ACP) {
459
+ acpStore.setACP(TEST_CHAIN_ID, account.address, acp);
460
+ acpStore.setActiveACPHash(TEST_CHAIN_ID, account.address, acp.hash);
461
+ }
462
+
463
+ function selectTxACP(selection: Selection, acp: ACP) {
464
+ const builder = createTxBuilder();
465
+ if (selection === 'acp hash') return builder.withACP(acp.hash);
466
+ if (selection === 'acp object') return builder.withACP(acp);
467
+ return builder.withACP();
468
+ }
469
+
470
+ function selectViewACP(selection: Selection, acp: ACP) {
471
+ const builder = createViewBuilder(FheTypes.Uint32);
472
+ if (selection === 'acp hash') return builder.withACP(acp.hash);
473
+ if (selection === 'acp object') return builder.withACP(acp);
474
+ return builder.withACP();
475
+ }
476
+
477
+ beforeEach(() => {
478
+ acpStore.store.setState({ acps: {}, activeACPHash: {} });
479
+ vi.mocked(tnDecryptV2).mockReset();
480
+ vi.mocked(tnSealOutputV2).mockReset();
481
+ });
482
+
483
+ afterEach(() => {
484
+ acpStore.store.setState({ acps: {}, activeACPHash: {} });
485
+ });
486
+
487
+ describe('decryptForTx', () => {
488
+ it.each(selections)('rejects an expired acp selected via %s without dispatching', async (selection) => {
489
+ const acp = createSignedSelfACP(now() - 3600);
490
+ seedACP(acp);
491
+
492
+ await expect(selectTxACP(selection, acp).execute()).rejects.toThrow('ACP is expired');
493
+ expect(tnDecryptV2).not.toHaveBeenCalled();
494
+ });
495
+
496
+ it('dispatches a valid acp (control for the expired cases)', async () => {
497
+ vi.mocked(tnDecryptV2).mockResolvedValue({ decryptedValue: 7n, signature: '0x1234' });
498
+ const acp = createSignedSelfACP(now() + 3600);
499
+ seedACP(acp);
500
+
501
+ const result = await selectTxACP('active acp', acp).execute();
502
+
503
+ expect(result.decryptedValue).toBe(7n);
504
+ expect(tnDecryptV2).toHaveBeenCalledOnce();
505
+ });
506
+ });
507
+
508
+ describe('decryptForView', () => {
509
+ it.each(selections)('rejects an expired acp selected via %s without dispatching', async (selection) => {
510
+ const acp = createSignedSelfACP(now() - 3600);
511
+ seedACP(acp);
512
+
513
+ await expect(selectViewACP(selection, acp).execute()).rejects.toThrow('ACP is expired');
514
+ expect(tnSealOutputV2).not.toHaveBeenCalled();
515
+ });
516
+
517
+ it('dispatches a valid acp (control for the expired cases)', async () => {
518
+ vi.mocked(tnSealOutputV2).mockRejectedValue(new Error('stubbed threshold network'));
519
+ const acp = createSignedSelfACP(now() + 3600);
520
+ seedACP(acp);
521
+
522
+ await expect(selectViewACP('active acp', acp).execute()).rejects.toThrow('stubbed threshold network');
523
+ expect(tnSealOutputV2).toHaveBeenCalledOnce();
524
+ });
525
+ });
526
+ });
@@ -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,
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { PublicClient } from 'viem';
3
+ import { acps } from '../acps';
4
+ import { UNLABELLED, encodeShareMetadata, shareAppKey } from '../../acps/share-metadata';
5
+ import { shareIdOfChainShare, type ChainShare, type ShareHeader } from '../../acps/registry';
6
+
7
+ const REGISTRY = '0x00000000000000000000000000000000000000e0';
8
+ const ISSUER = '0x00000000000000000000000000000000000000b0';
9
+ const RECIPIENT = '0x00000000000000000000000000000000000000a1';
10
+ const ZERO = '0x0000000000000000000000000000000000000000';
11
+
12
+ const chainShare = (handle: number): ChainShare => ({
13
+ issuer: ISSUER,
14
+ expiration: 2_000_000_000n,
15
+ recipient: RECIPIENT,
16
+ revokerData: 0n,
17
+ revokerContract: ZERO,
18
+ scope: 2,
19
+ contracts: [],
20
+ handles: [`0x${handle.toString(16).padStart(64, '0')}`],
21
+ sealingKey: `0x${'0'.repeat(64)}`,
22
+ issuerSignature: '0x11',
23
+ recipientSignature: '0x',
24
+ });
25
+
26
+ const header: ShareHeader = {
27
+ issuer: ISSUER,
28
+ expiration: 2_000_000_000n,
29
+ recipient: RECIPIENT,
30
+ blockNumber: 10n,
31
+ revokerContract: ZERO,
32
+ revokerData: 0n,
33
+ };
34
+
35
+ const EXPLORER = shareAppKey('explorer.fhenix.io');
36
+ const WALLET = shareAppKey('wallet.example');
37
+
38
+ /** Three shares in one block: one for the explorer, one for a wallet app, one for no app. */
39
+ const shares = [chainShare(1), chainShare(2), chainShare(3)];
40
+ const ids = shares.map(shareIdOfChainShare);
41
+ const metadata = [
42
+ encodeShareMetadata([UNLABELLED], { appKey: EXPLORER }),
43
+ encodeShareMetadata([UNLABELLED], { appKey: WALLET }),
44
+ '0x',
45
+ ] as const;
46
+
47
+ const client = {
48
+ readContract: vi.fn(async () => [ids, shares.map(() => header)]),
49
+ getContractEvents: vi.fn(async ({ args }: { args: { shareId: string[] } }) =>
50
+ shares
51
+ .map((acp, i) => ({ args: { shareId: ids[i], acp, metadata: metadata[i] } }))
52
+ .filter((e) => args.shareId.includes(e.args.shareId))
53
+ ),
54
+ } as unknown as PublicClient;
55
+
56
+ describe('getIncomingShares and the app key', () => {
57
+ it('lists every share without an app, each with the key it carries', async () => {
58
+ const incoming = await acps.getIncomingShares(client, REGISTRY, RECIPIENT);
59
+ expect(incoming.map((s) => s.shareId)).toEqual(ids);
60
+ expect(incoming.map((s) => s.appKey)).toEqual([EXPLORER, WALLET, undefined]);
61
+ });
62
+
63
+ it('lists only the shares posted for the app', async () => {
64
+ const incoming = await acps.getIncomingShares(client, REGISTRY, RECIPIENT, { appKey: EXPLORER });
65
+ expect(incoming.map((s) => s.shareId)).toEqual([ids[0]]);
66
+ });
67
+
68
+ it('leaves out a share whose event the node cannot serve yet, and keeps the rest', async () => {
69
+ const lagging = {
70
+ readContract: vi.fn(async () => [ids, shares.map(() => header)]),
71
+ getContractEvents: vi.fn(async ({ args }: { args: { shareId: string[] } }) =>
72
+ shares
73
+ .map((acp, i) => ({ args: { shareId: ids[i], acp, metadata: metadata[i] } }))
74
+ .filter((e) => args.shareId.includes(e.args.shareId) && e.args.shareId !== ids[1])
75
+ ),
76
+ } as unknown as PublicClient;
77
+ const incoming = await acps.getIncomingShares(lagging, REGISTRY, RECIPIENT);
78
+ expect(incoming.map((s) => s.shareId)).toEqual([ids[0], ids[2]]);
79
+ });
80
+
81
+ it('lists nothing for an app no share names', async () => {
82
+ expect(await acps.getIncomingShares(client, REGISTRY, RECIPIENT, { appKey: shareAppKey('other') })).toEqual([]);
83
+ });
84
+ });
@@ -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,90 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { type PublicClient, type WalletClient } from 'viem';
3
+ import { hardhat } from 'viem/chains';
4
+ import { EncryptInputsBuilder } from '../encrypt/encryptInputsBuilder.js';
5
+ import { Encryptable, FheTypes, type EncryptableItem } from '../types.js';
6
+ import { createCofheConfigBase } from '../config.js';
7
+ import { zkPack, type ZkCiphertextListBuilder } from '../encrypt/zkPackProveVerify.js';
8
+
9
+ // The hardhat (mocks) encrypt path must reject the same out-of-range plaintexts that the
10
+ // production path rejects in zkPack. Otherwise MockCoFHE._set masks the value with the utype
11
+ // mask (e.g. uint8(256) -> 0) and tests pass on hardhat while the same call throws on a real chain.
12
+
13
+ const account = '0x1234567890123456789012345678901234567890';
14
+ const consumingContract = '0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef';
15
+
16
+ const fakeClients = () => {
17
+ const readContract = vi.fn(async ({ args }: any) => (args[0] as bigint[]).map((_, i) => BigInt(i + 1)));
18
+ const writeContract = vi.fn(async () => '0x' as `0x${string}`);
19
+ const publicClient = { readContract } as unknown as PublicClient;
20
+ const walletClient = { account: { address: account }, writeContract } as unknown as WalletClient;
21
+ return { publicClient, walletClient, readContract, writeContract };
22
+ };
23
+
24
+ const mocksBuilder = (inputs: EncryptableItem[]) => {
25
+ const { publicClient, walletClient, readContract, writeContract } = fakeClients();
26
+ const builder = new EncryptInputsBuilder({
27
+ inputs: inputs as any,
28
+ account,
29
+ chainId: hardhat.id,
30
+ config: createCofheConfigBase({ supportedChains: [], mocks: { encryptDelay: 0 } }),
31
+ publicClient,
32
+ walletClient,
33
+ zkvWalletClient: walletClient,
34
+ tfhePublicKeyDeserializer: vi.fn(),
35
+ compactPkeCrsDeserializer: vi.fn(),
36
+ zkBuilderAndCrsGenerator: vi.fn() as any,
37
+ initTfhe: undefined,
38
+ zkProveWorkerFn: undefined,
39
+ keysStorage: undefined,
40
+ requireConnected: vi.fn(),
41
+ });
42
+ builder.setConsumingContract(consumingContract);
43
+ return { builder, readContract, writeContract };
44
+ };
45
+
46
+ const noop = () => undefined;
47
+ const noopZkBuilder = {
48
+ push_boolean: noop,
49
+ push_u8: noop,
50
+ push_u16: noop,
51
+ push_u32: noop,
52
+ push_u64: noop,
53
+ push_u128: noop,
54
+ push_u160: noop,
55
+ build_with_proof_packed: noop,
56
+ } as unknown as ZkCiphertextListBuilder;
57
+
58
+ const outOfRange: [string, EncryptableItem][] = [
59
+ ['uint8(256)', Encryptable.uint8(256n)],
60
+ ['uint16(65536)', Encryptable.uint16(65536n)],
61
+ ['uint32(2^32)', Encryptable.uint32(2n ** 32n)],
62
+ ['uint64(2^64)', Encryptable.uint64(2n ** 64n)],
63
+ ['uint128(2^128)', Encryptable.uint128(2n ** 128n)],
64
+ ['address(2^160)', { data: 2n ** 160n, securityZone: 0, utype: FheTypes.Uint160 } as EncryptableItem],
65
+ ];
66
+
67
+ describe('mocks encrypt path value range parity', () => {
68
+ it.each(outOfRange)('production zkPack rejects %s', (_, item) => {
69
+ expect(() => zkPack([item], noopZkBuilder)).toThrow(/out of range/);
70
+ });
71
+
72
+ it.each(outOfRange)('hardhat mocks encrypt rejects %s before touching the chain', async (_, item) => {
73
+ const { builder, readContract, writeContract } = mocksBuilder([item]);
74
+ await expect(builder.execute()).rejects.toThrow(/out of range/);
75
+ expect(readContract).not.toHaveBeenCalled();
76
+ expect(writeContract).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it('hardhat mocks encrypt still accepts the max value of each type', async () => {
80
+ const { builder, writeContract } = mocksBuilder([
81
+ Encryptable.uint8(255n),
82
+ Encryptable.uint16(65535n),
83
+ Encryptable.uint32(2n ** 32n - 1n),
84
+ Encryptable.uint64(2n ** 64n - 1n),
85
+ Encryptable.bool(true),
86
+ ]);
87
+ await expect(builder.execute()).resolves.toBeDefined();
88
+ expect(writeContract).toHaveBeenCalledTimes(1);
89
+ });
90
+ });
@@ -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
+ });
@@ -0,0 +1,128 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ fromHexString,
4
+ toHexString,
5
+ hexToBytes,
6
+ toBigIntOrThrow,
7
+ validateBigIntInRange,
8
+ fheTypeToString,
9
+ } from '../utils.js';
10
+ import { FheTypes } from '../types.js';
11
+
12
+ describe('fromHexString', () => {
13
+ it('strips the 0x prefix before padding an odd-length string', () => {
14
+ // Regression: padding first produced `00x123`, which decoded to [0x00, NaN->0, 0x23].
15
+ expect(Array.from(fromHexString('0x123'))).toEqual([0x01, 0x23]);
16
+ });
17
+
18
+ it('pads odd-length strings without a prefix', () => {
19
+ expect(Array.from(fromHexString('123'))).toEqual([0x01, 0x23]);
20
+ });
21
+
22
+ it('decodes even-length strings with and without the 0x prefix identically', () => {
23
+ expect(Array.from(fromHexString('0x1234'))).toEqual([0x12, 0x34]);
24
+ expect(Array.from(fromHexString('1234'))).toEqual([0x12, 0x34]);
25
+ });
26
+
27
+ it('decodes uppercase hex', () => {
28
+ expect(Array.from(fromHexString('0xDEADBEEF'))).toEqual([0xde, 0xad, 0xbe, 0xef]);
29
+ });
30
+
31
+ it('returns an empty array for empty input', () => {
32
+ expect(fromHexString('')).toEqual(new Uint8Array());
33
+ expect(fromHexString('0x')).toEqual(new Uint8Array());
34
+ });
35
+
36
+ it('round-trips through toHexString', () => {
37
+ const hex = 'a3'.repeat(32);
38
+ const bytes = fromHexString(hex);
39
+
40
+ expect(bytes.length).toBe(32);
41
+ expect(toHexString(bytes)).toBe(hex);
42
+ });
43
+
44
+ it('throws instead of silently producing zero bytes for non-hex characters', () => {
45
+ expect(() => fromHexString('zzzz')).toThrow('Invalid hex string');
46
+ });
47
+
48
+ it('throws for hex strings with a single invalid character', () => {
49
+ expect(() => fromHexString('0x12g4')).toThrow('Invalid hex string');
50
+ });
51
+
52
+ it('only strips a lowercase 0x prefix', () => {
53
+ expect(() => fromHexString('0X12')).toThrow('Invalid hex string');
54
+ });
55
+
56
+ it('does not leak sensitive input in the error message', () => {
57
+ const secret = 'f'.repeat(63) + 'z';
58
+
59
+ expect(() => fromHexString(secret)).toThrow(
60
+ expect.objectContaining({ message: expect.not.stringContaining(secret) })
61
+ );
62
+ });
63
+ });
64
+
65
+ describe('hexToBytes', () => {
66
+ it('decodes even-length hex with and without prefix', () => {
67
+ expect(Array.from(hexToBytes('0x1234'))).toEqual([0x12, 0x34]);
68
+ expect(Array.from(hexToBytes('1234'))).toEqual([0x12, 0x34]);
69
+ });
70
+
71
+ it('handles odd-length hex correctly with prefix', () => {
72
+ expect(Array.from(hexToBytes('0x123'))).toEqual([0x01, 0x23]);
73
+ expect(Array.from(hexToBytes('123'))).toEqual([0x01, 0x23]);
74
+ });
75
+
76
+ it('throws for invalid hex characters', () => {
77
+ expect(() => hexToBytes('0x12z4')).toThrow('Invalid hex string');
78
+ });
79
+ });
80
+
81
+ describe('toHexString', () => {
82
+ it('pads each byte to 2 hex characters', () => {
83
+ const bytes = new Uint8Array([0x00, 0x0f, 0x10, 0xff]);
84
+ expect(toHexString(bytes)).toBe('000f10ff');
85
+ });
86
+ });
87
+
88
+ describe('toBigIntOrThrow', () => {
89
+ it('returns bigint unchanged if passed bigint', () => {
90
+ expect(toBigIntOrThrow(123n)).toBe(123n);
91
+ });
92
+
93
+ it('converts valid string to bigint', () => {
94
+ expect(toBigIntOrThrow('456')).toBe(456n);
95
+ expect(toBigIntOrThrow('0x10')).toBe(16n);
96
+ });
97
+
98
+ it('throws for invalid string', () => {
99
+ expect(() => toBigIntOrThrow('not-a-number')).toThrow('Invalid input: Unable to convert to bigint');
100
+ });
101
+ });
102
+
103
+ describe('validateBigIntInRange', () => {
104
+ it('accepts values within range', () => {
105
+ expect(() => validateBigIntInRange(10n, 20n, 0n)).not.toThrow();
106
+ });
107
+
108
+ it('throws when value exceeds max', () => {
109
+ expect(() => validateBigIntInRange(25n, 20n, 0n)).toThrow('Value out of range');
110
+ });
111
+
112
+ it('throws when value is below min', () => {
113
+ expect(() => validateBigIntInRange(5n, 20n, 10n)).toThrow('Value out of range');
114
+ });
115
+ });
116
+
117
+ describe('fheTypeToString', () => {
118
+ it('maps known types to string identifiers', () => {
119
+ expect(fheTypeToString(FheTypes.Bool)).toBe('bool');
120
+ expect(fheTypeToString(FheTypes.Uint8)).toBe('uint8');
121
+ expect(fheTypeToString(FheTypes.Uint16)).toBe('uint16');
122
+ expect(fheTypeToString(FheTypes.Uint32)).toBe('uint32');
123
+ expect(fheTypeToString(FheTypes.Uint64)).toBe('uint64');
124
+ expect(fheTypeToString(FheTypes.Uint128)).toBe('uint128');
125
+ expect(fheTypeToString(FheTypes.Uint160)).toBe('uint160');
126
+ expect(fheTypeToString(FheTypes.Uint256)).toBe('uint256');
127
+ });
128
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { encodePacked, keccak256, zeroAddress, type Address, type PublicClient } from 'viem';
3
+ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
4
+
5
+ import { verifyDecryptResult } from '../decrypt/verifyDecryptResult.js';
6
+ import { clearTaskManagerAddressCache } from '../taskManager.js';
7
+
8
+ const CHAIN_ID = 31337;
9
+ const TM = '0x0000000000000000000000000000000000005000';
10
+ // euint32 (utype 4) in the type byte
11
+ const CT_HASH = (0xabcdefn << 16n) | (4n << 8n);
12
+ const CLEARTEXT = 42n;
13
+
14
+ const signer = privateKeyToAccount(generatePrivateKey());
15
+
16
+ const signDecryptResult = (ctHash: bigint, cleartext: bigint) =>
17
+ signer.sign({
18
+ hash: keccak256(encodePacked(['uint256', 'uint32', 'uint64', 'uint256'], [cleartext, 4, BigInt(CHAIN_ID), ctHash])),
19
+ });
20
+
21
+ const fakeClient = (decryptResultSigner: Address) =>
22
+ ({
23
+ chain: { id: CHAIN_ID },
24
+ readContract: vi.fn(async ({ functionName }: { functionName: string }) =>
25
+ functionName === 'getTm' ? TM : decryptResultSigner
26
+ ),
27
+ }) as unknown as PublicClient;
28
+
29
+ describe('verifyDecryptResult', () => {
30
+ beforeEach(() => clearTaskManagerAddressCache());
31
+
32
+ it('accepts a result signed by the configured decryptResultSigner', async () => {
33
+ const signature = await signDecryptResult(CT_HASH, CLEARTEXT);
34
+
35
+ expect(await verifyDecryptResult(CT_HASH, CLEARTEXT, signature, fakeClient(signer.address))).to.equal(true);
36
+ });
37
+
38
+ it('rejects a result signed by a different key', async () => {
39
+ const signature = await signDecryptResult(CT_HASH, CLEARTEXT);
40
+ const other = privateKeyToAccount(generatePrivateKey());
41
+
42
+ expect(await verifyDecryptResult(CT_HASH, CLEARTEXT, signature, fakeClient(other.address))).to.equal(false);
43
+ });
44
+
45
+ it('fails closed when the decryptResultSigner is unset (zero address)', async () => {
46
+ const signature = await signDecryptResult(CT_HASH, CLEARTEXT);
47
+
48
+ expect(await verifyDecryptResult(CT_HASH, CLEARTEXT, signature, fakeClient(zeroAddress))).to.equal(false);
49
+ expect(await verifyDecryptResult(CT_HASH, CLEARTEXT, '0x', fakeClient(zeroAddress))).to.equal(false);
50
+ });
51
+ });
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>;