@cofhe/sdk 0.8.0 → 1.0.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 +51 -0
- package/acps/acp.ts +25 -6
- package/acps/index.ts +20 -15
- package/acps/onchain-utils.ts +15 -10
- package/acps/registry.ts +166 -8
- package/acps/sealing.ts +31 -5
- package/acps/share-metadata/appKey.ts +8 -0
- package/acps/share-metadata/decode.ts +120 -0
- package/acps/share-metadata/describe.ts +198 -0
- package/acps/share-metadata/encode.ts +147 -0
- package/acps/share-metadata/erc7984.ts +26 -0
- package/acps/share-metadata/index.ts +9 -0
- package/acps/share-metadata/layout.ts +69 -0
- package/acps/share-metadata/presets.ts +116 -0
- package/acps/share-metadata/schema.ts +47 -0
- package/acps/share-metadata/test/codec.test.ts +140 -0
- package/acps/share-metadata/test/describe.test.ts +112 -0
- package/acps/share-metadata/test/fixtures.ts +157 -0
- package/acps/share-metadata/test/live.test.ts +56 -0
- package/acps/share-metadata/test/presets.test.ts +119 -0
- package/acps/share-metadata/test/verify.test.ts +144 -0
- package/acps/share-metadata/verify.ts +155 -0
- package/acps/store.ts +48 -18
- package/acps/test/aclServedAddresses.test.ts +65 -0
- package/acps/test/acp.test.ts +102 -7
- package/acps/test/localstorage.test.ts +28 -1
- package/acps/test/registry.test.ts +144 -0
- package/acps/test/sealing.test.ts +74 -0
- package/acps/test/store.test.ts +71 -0
- package/acps/test/utils.test.ts +63 -0
- package/acps/test/validation.test.ts +0 -4
- package/acps/types.ts +43 -6
- package/acps/utils.ts +15 -2
- package/acps/validation.ts +3 -1
- package/core/acps.ts +123 -74
- package/core/client.ts +33 -7
- package/core/clientTypes.ts +25 -7
- package/core/config.ts +8 -0
- package/core/decrypt/apiError.ts +4 -1
- package/core/decrypt/verifyDecryptResult.ts +4 -1
- package/core/encrypt/cofheMocksZkVerifySign.ts +19 -2
- package/core/test/acpDefaults.test.ts +108 -19
- package/core/test/acpStoreAccountCase.test.ts +80 -0
- package/core/test/acps.test.ts +38 -3
- package/core/test/apiError.test.ts +8 -0
- package/core/test/appKeyConfig.test.ts +94 -0
- package/core/test/config.test.ts +8 -0
- package/core/test/decryptBuilders.test.ts +110 -2
- package/core/test/incomingShares.test.ts +84 -0
- package/core/test/mocksEncryptRange.test.ts +90 -0
- package/core/test/utils.test.ts +128 -0
- package/core/test/verifyDecryptResult.test.ts +51 -0
- package/core/utils.ts +16 -9
- package/dist/acps.cjs +796 -132
- package/dist/acps.d.cts +367 -35
- package/dist/acps.d.ts +367 -35
- package/dist/acps.js +2 -2
- package/dist/{chunk-UJYOBHYA.js → chunk-3L2IVPMI.js} +688 -203
- package/dist/chunk-J23FYAI2.js +174 -0
- package/dist/{chunk-GGI7Y6AW.js → chunk-O2ITEUEU.js} +110 -269
- package/dist/chunk-WJGTPW7K.js +228 -0
- package/dist/{clientTypes-D3aIXmUp.d.cts → clientTypes-IO6b5Um8.d.cts} +61 -21
- package/dist/{clientTypes-5YPPR16m.d.ts → clientTypes-b1pOxOUE.d.ts} +61 -21
- package/dist/core.cjs +3115 -2584
- package/dist/core.d.cts +6 -3
- package/dist/core.d.ts +6 -3
- package/dist/core.js +4 -3
- package/dist/node.cjs +3001 -2470
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +18 -17
- package/dist/{onchain-utils-CUAK8glx.d.cts → onchain-utils-CgOslubH.d.cts} +139 -15
- package/dist/{onchain-utils-CUAK8glx.d.ts → onchain-utils-CgOslubH.d.ts} +139 -15
- package/dist/web.cjs +3011 -2487
- package/dist/web.d.cts +2 -2
- package/dist/web.d.ts +2 -2
- package/dist/web.js +4 -10
- package/dist/zkProve.worker.cjs +14 -7
- package/dist/zkProve.worker.js +2 -8
- package/node/index.ts +1 -10
- package/node/storage.ts +18 -7
- package/node/test/storage-permissions.test.ts +40 -0
- package/package.json +4 -4
- package/web/index.ts +1 -10
- package/web/test/threadPoolBenchHelper.ts +1 -7
- package/web/zkProve.worker.ts +1 -10
- package/dist/chunk-NMKMCAI3.js +0 -14
package/acps/test/acp.test.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { UNLABELLED, decodeShareAppKey, shareAppKey } from '../share-metadata';
|
|
2
3
|
import {
|
|
3
4
|
ACPUtils,
|
|
5
|
+
encodeShareMetadata,
|
|
4
6
|
type CreateSelfACPOptions,
|
|
5
7
|
type CreateSharingACPOptions,
|
|
6
8
|
type ImportSharedACPOptions,
|
|
9
|
+
type ShareLabel,
|
|
7
10
|
} from '../index.js';
|
|
8
11
|
|
|
9
12
|
// ACP domain resolution requires an upgraded on-chain ACL (domain ("ACL","2") served by the ACL itself).
|
|
@@ -86,6 +89,21 @@ describe('ACPUtils Tests', () => {
|
|
|
86
89
|
});
|
|
87
90
|
|
|
88
91
|
describe('createSharing', () => {
|
|
92
|
+
it('keeps the app key in the metadata, with the labels or with the handles listed as unlabelled', () => {
|
|
93
|
+
const handle = `0x${'aa'.repeat(32)}` as const;
|
|
94
|
+
const appKey = shareAppKey('explorer.fhenix.io');
|
|
95
|
+
const base = { type: 'sharing' as const, issuer: bobAddress, recipient: aliceAddress, handles: [handle] };
|
|
96
|
+
|
|
97
|
+
const keyOnly = ACPUtils.createSharing({ ...base, appKey });
|
|
98
|
+
expect(keyOnly.metadata).toBe(encodeShareMetadata([UNLABELLED], { appKey }));
|
|
99
|
+
expect(decodeShareAppKey(keyOnly.metadata, [handle])).toBe(appKey);
|
|
100
|
+
|
|
101
|
+
const labelled = ACPUtils.createSharing({ ...base, appKey, labels: [UNLABELLED] });
|
|
102
|
+
expect(labelled.metadata).toBe(keyOnly.metadata);
|
|
103
|
+
|
|
104
|
+
expect(ACPUtils.createSharing(base).metadata).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
89
107
|
it('should create a sharing acp with valid options', async () => {
|
|
90
108
|
const options: CreateSharingACPOptions = {
|
|
91
109
|
type: 'sharing',
|
|
@@ -129,14 +147,13 @@ describe('ACPUtils Tests', () => {
|
|
|
129
147
|
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
130
148
|
recipient: aliceAddress,
|
|
131
149
|
issuerSignature: '0x1234567890abcdef',
|
|
132
|
-
name: 'Test Import ACP',
|
|
133
150
|
};
|
|
134
151
|
|
|
135
152
|
const acp = ACPUtils.importShared(options);
|
|
136
153
|
|
|
137
154
|
expect(acp.hash).toBe(ACPUtils.getHash(acp));
|
|
138
155
|
expect(acp.type).toBe('recipient');
|
|
139
|
-
expect(acp.name).toBe(
|
|
156
|
+
expect(acp.name).toBe(`Shared by ${bobAddress.slice(0, 6)}…${bobAddress.slice(-4)}`);
|
|
140
157
|
expect(acp.issuer).toBe(bobAddress);
|
|
141
158
|
expect(acp.recipient).toBe(aliceAddress);
|
|
142
159
|
expect(acp.issuerSignature).toBe('0x1234567890abcdef');
|
|
@@ -147,6 +164,19 @@ describe('ACPUtils Tests', () => {
|
|
|
147
164
|
expect(acp.recipientSignature).toBe('0x');
|
|
148
165
|
});
|
|
149
166
|
|
|
167
|
+
it("ignores the issuer's name in the share; the recipient may give their own", async () => {
|
|
168
|
+
const share = JSON.stringify({
|
|
169
|
+
issuer: bobAddress,
|
|
170
|
+
expiration: Math.floor(Date.now() / 1000) + 3600,
|
|
171
|
+
recipient: aliceAddress,
|
|
172
|
+
issuerSignature: '0x1234567890abcdef',
|
|
173
|
+
name: "Bob's note",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(ACPUtils.importShared(share).name).toBe(`Shared by ${bobAddress.slice(0, 6)}…${bobAddress.slice(-4)}`);
|
|
177
|
+
expect(ACPUtils.importShared(share, { name: ' Payroll from Bob ' }).name).toBe('Payroll from Bob');
|
|
178
|
+
});
|
|
179
|
+
|
|
150
180
|
it('should import a shared acp with valid options as string', async () => {
|
|
151
181
|
const options: ImportSharedACPOptions = {
|
|
152
182
|
issuer: bobAddress,
|
|
@@ -188,7 +218,6 @@ describe('ACPUtils Tests', () => {
|
|
|
188
218
|
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
189
219
|
recipient: aliceAddress,
|
|
190
220
|
issuerSignature: '0x', // Invalid empty signature
|
|
191
|
-
name: 'Test Import ACP',
|
|
192
221
|
};
|
|
193
222
|
|
|
194
223
|
expect(() => ACPUtils.importShared(options)).toThrow();
|
|
@@ -246,7 +275,6 @@ describe('ACPUtils Tests', () => {
|
|
|
246
275
|
recipient: aliceAddress,
|
|
247
276
|
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
248
277
|
issuerSignature: '0x1234567890abcdef',
|
|
249
|
-
name: 'Test Import ACP',
|
|
250
278
|
};
|
|
251
279
|
|
|
252
280
|
const acp = await ACPUtils.importSharedAndSign(options, publicClient, aliceWalletClient);
|
|
@@ -315,7 +343,6 @@ describe('ACPUtils Tests', () => {
|
|
|
315
343
|
recipient: aliceAddress,
|
|
316
344
|
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
317
345
|
issuerSignature: '0x1111111111111111111111111111111111111111111111111111111111111111',
|
|
318
|
-
name: 'Test ACP',
|
|
319
346
|
});
|
|
320
347
|
|
|
321
348
|
const signedACP = await ACPUtils.sign(acp, publicClient, aliceWalletClient);
|
|
@@ -431,7 +458,7 @@ describe('ACPUtils Tests', () => {
|
|
|
431
458
|
const exported = ACPUtils.export(acp);
|
|
432
459
|
const parsed = JSON.parse(exported);
|
|
433
460
|
|
|
434
|
-
expect(parsed
|
|
461
|
+
expect(parsed).not.toHaveProperty('name'); // the issuer's own note stays with them
|
|
435
462
|
expect(parsed.type).toBe('sharing');
|
|
436
463
|
expect(parsed.issuer).toBe(bobAddress);
|
|
437
464
|
expect(parsed.recipient).toBe(aliceAddress);
|
|
@@ -456,7 +483,6 @@ describe('ACPUtils Tests', () => {
|
|
|
456
483
|
|
|
457
484
|
expect(Object.keys(parsed).sort()).toEqual(
|
|
458
485
|
[
|
|
459
|
-
'name',
|
|
460
486
|
'type',
|
|
461
487
|
'issuer',
|
|
462
488
|
'expiration',
|
|
@@ -475,6 +501,7 @@ describe('ACPUtils Tests', () => {
|
|
|
475
501
|
expect(parsed.handles).toEqual([]);
|
|
476
502
|
expect(parsed.revokerData).toBe(0);
|
|
477
503
|
// private component never leaves the client
|
|
504
|
+
expect(parsed).not.toHaveProperty('name');
|
|
478
505
|
expect(parsed).not.toHaveProperty('sealingPrivateKey');
|
|
479
506
|
expect(parsed).not.toHaveProperty('sealingKey');
|
|
480
507
|
expect(parsed).not.toHaveProperty('hash');
|
|
@@ -482,6 +509,74 @@ describe('ACPUtils Tests', () => {
|
|
|
482
509
|
});
|
|
483
510
|
});
|
|
484
511
|
|
|
512
|
+
describe('share labels in exported JSON', () => {
|
|
513
|
+
const HANDLE = `0x${'ab'.repeat(32)}` as const;
|
|
514
|
+
const label: ShareLabel = {
|
|
515
|
+
kind: 'stored',
|
|
516
|
+
contract: '0x2222222222222222222222222222222222222222',
|
|
517
|
+
selector: '0x344ff101',
|
|
518
|
+
args: [{ type: 'issuer' }],
|
|
519
|
+
returnWord: 0,
|
|
520
|
+
block: 123n,
|
|
521
|
+
};
|
|
522
|
+
const signed = (acp: ReturnType<typeof ACPUtils.createSharing>) => ({
|
|
523
|
+
...acp,
|
|
524
|
+
issuerSignature: `0x${'11'.repeat(65)}` as const,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('createSharing keeps the labels on the acp as metadata', () => {
|
|
528
|
+
const acp = ACPUtils.createSharing({
|
|
529
|
+
issuer: bobAddress,
|
|
530
|
+
recipient: aliceAddress,
|
|
531
|
+
handles: [HANDLE],
|
|
532
|
+
labels: [label],
|
|
533
|
+
});
|
|
534
|
+
expect(acp.metadata).toBe(encodeShareMetadata([label]));
|
|
535
|
+
expect(ACPUtils.deserialize(ACPUtils.serialize(acp)).metadata).toBe(acp.metadata);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('createSharing rejects a label count that does not match the handles', () => {
|
|
539
|
+
expect(() =>
|
|
540
|
+
ACPUtils.createSharing({
|
|
541
|
+
issuer: bobAddress,
|
|
542
|
+
recipient: aliceAddress,
|
|
543
|
+
handles: [HANDLE],
|
|
544
|
+
labels: [label, label],
|
|
545
|
+
})
|
|
546
|
+
).toThrow('Cannot label a share of 1 handles with 2 labels');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('export writes the metadata and importShared keeps it on the recipient acp', () => {
|
|
550
|
+
const sharing = signed(
|
|
551
|
+
ACPUtils.createSharing({ issuer: bobAddress, recipient: aliceAddress, handles: [HANDLE], labels: [label] })
|
|
552
|
+
);
|
|
553
|
+
const json = ACPUtils.export(sharing);
|
|
554
|
+
expect(JSON.parse(json).metadata).toBe(sharing.metadata);
|
|
555
|
+
|
|
556
|
+
const recipient = ACPUtils.importShared(json);
|
|
557
|
+
expect(recipient.metadata).toBe(sharing.metadata);
|
|
558
|
+
expect(recipient.hash).toBe(ACPUtils.importShared({ ...JSON.parse(json), metadata: undefined }).hash);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('imports JSON without metadata, or with an empty one, as a share without labels', () => {
|
|
562
|
+
const sharing = signed(
|
|
563
|
+
ACPUtils.createSharing({ issuer: bobAddress, recipient: aliceAddress, handles: [HANDLE] })
|
|
564
|
+
);
|
|
565
|
+
const json = ACPUtils.export(sharing);
|
|
566
|
+
expect(JSON.parse(json)).not.toHaveProperty('metadata');
|
|
567
|
+
|
|
568
|
+
expect(ACPUtils.importShared(json).metadata).toBeUndefined();
|
|
569
|
+
expect(ACPUtils.importShared({ ...JSON.parse(json), metadata: '0x' }).metadata).toBeUndefined();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('importShared rejects metadata that is not hex', () => {
|
|
573
|
+
const json = ACPUtils.export(
|
|
574
|
+
signed(ACPUtils.createSharing({ issuer: bobAddress, recipient: aliceAddress, handles: [HANDLE] }))
|
|
575
|
+
);
|
|
576
|
+
expect(() => ACPUtils.importShared({ ...JSON.parse(json), metadata: 'labels' })).toThrow(/metadata/);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
485
580
|
describe('updateName', () => {
|
|
486
581
|
it('should update acp name immutably', async () => {
|
|
487
582
|
const acp = ACPUtils.createSelf({
|
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getACP,
|
|
8
|
+
setACP,
|
|
9
|
+
removeACP,
|
|
10
|
+
getActiveACPHash,
|
|
11
|
+
setActiveACPHash,
|
|
12
|
+
ACPUtils,
|
|
13
|
+
acpStore,
|
|
14
|
+
ACP_STORE_DEFAULTS,
|
|
15
|
+
} from '../index.js';
|
|
7
16
|
import { createMockACP } from '../test-utils.js';
|
|
8
17
|
|
|
9
18
|
// Type declarations for happy-dom environment
|
|
@@ -102,4 +111,22 @@ describe('ACPs localStorage Tests', () => {
|
|
|
102
111
|
expect(parsedData.state.acps[chainId][account][acp.hash]).toBeUndefined();
|
|
103
112
|
expect(parsedData.state.activeACPHash[chainId][account]).toBeUndefined();
|
|
104
113
|
});
|
|
114
|
+
|
|
115
|
+
it('should recover from a corrupt store persisted at the current version', async () => {
|
|
116
|
+
// `migrate` only runs for older versions, so a corrupt state saved at the current
|
|
117
|
+
// version reaches the store unless it is cleaned on rehydrate.
|
|
118
|
+
localStorage.setItem(
|
|
119
|
+
'cofhesdk-acps',
|
|
120
|
+
JSON.stringify({ state: { acps: null, activeACPHash: {} }, version: acpStore.store.persist.getOptions().version })
|
|
121
|
+
);
|
|
122
|
+
await acpStore.store.persist.rehydrate();
|
|
123
|
+
|
|
124
|
+
// Fixed on load, so consumers that read the raw snapshot (the React hooks) are covered too.
|
|
125
|
+
expect(acpStore.store.getState()).toEqual(ACP_STORE_DEFAULTS);
|
|
126
|
+
const snapshot = acpStore.store.getState();
|
|
127
|
+
expect(() => snapshot.acps[chainId]?.[account]).not.toThrow();
|
|
128
|
+
|
|
129
|
+
expect(() => getACP(chainId, account, '0xdeadbeef')).not.toThrow();
|
|
130
|
+
expect(getACP(chainId, account, '0xdeadbeef')).toBeUndefined();
|
|
131
|
+
});
|
|
105
132
|
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { PublicClient } from 'viem';
|
|
3
|
+
import {
|
|
4
|
+
readPostedShares,
|
|
5
|
+
readShare,
|
|
6
|
+
readSharesFor,
|
|
7
|
+
shareIdOfChainShare,
|
|
8
|
+
type ChainShare,
|
|
9
|
+
type ShareHeader,
|
|
10
|
+
} from '../registry';
|
|
11
|
+
|
|
12
|
+
const REGISTRY = '0x00000000000000000000000000000000000000e0';
|
|
13
|
+
const ISSUER = '0x00000000000000000000000000000000000000b0';
|
|
14
|
+
const RECIPIENT = '0x00000000000000000000000000000000000000a1';
|
|
15
|
+
const ZERO = '0x0000000000000000000000000000000000000000';
|
|
16
|
+
|
|
17
|
+
const chainShare = (handle: number): ChainShare => ({
|
|
18
|
+
issuer: ISSUER,
|
|
19
|
+
expiration: 2_000_000_000n,
|
|
20
|
+
recipient: RECIPIENT,
|
|
21
|
+
revokerData: 0n,
|
|
22
|
+
revokerContract: ZERO,
|
|
23
|
+
scope: 2,
|
|
24
|
+
contracts: [],
|
|
25
|
+
handles: [`0x${handle.toString(16).padStart(64, '0')}`],
|
|
26
|
+
sealingKey: `0x${'0'.repeat(64)}`,
|
|
27
|
+
issuerSignature: '0x11',
|
|
28
|
+
recipientSignature: '0x',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const headerAt = (blockNumber: bigint): ShareHeader => ({
|
|
32
|
+
issuer: ISSUER,
|
|
33
|
+
expiration: 2_000_000_000n,
|
|
34
|
+
recipient: RECIPIENT,
|
|
35
|
+
blockNumber,
|
|
36
|
+
revokerContract: ZERO,
|
|
37
|
+
revokerData: 0n,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Three shares: two posted in block 10, one in block 20. */
|
|
41
|
+
const shares = [chainShare(1), chainShare(2), chainShare(3)];
|
|
42
|
+
const ids = shares.map(shareIdOfChainShare);
|
|
43
|
+
const headers = [headerAt(10n), headerAt(10n), headerAt(20n)];
|
|
44
|
+
const metadata: `0x${string}`[] = ['0x03aa', '0x', '0x03bb'];
|
|
45
|
+
|
|
46
|
+
const clientWith = (
|
|
47
|
+
events = shares.map((acp, i) => ({ block: headers[i].blockNumber, acp, id: ids[i], metadata: metadata[i] }))
|
|
48
|
+
) => {
|
|
49
|
+
const getContractEvents = vi.fn(async ({ fromBlock, args }: { fromBlock: bigint; args: { shareId: string[] } }) =>
|
|
50
|
+
events
|
|
51
|
+
.filter((e) => e.block === fromBlock && args.shareId.includes(e.id))
|
|
52
|
+
.map((e) => ({ args: { shareId: e.id, acp: e.acp, metadata: e.metadata } }))
|
|
53
|
+
);
|
|
54
|
+
const readContract = vi.fn(async ({ functionName, args }: { functionName: string; args: [string] }) => {
|
|
55
|
+
if (functionName === 'sharesFor') return [ids, headers];
|
|
56
|
+
const i = ids.indexOf(args[0] as `0x${string}`);
|
|
57
|
+
return i === -1 ? { ...headerAt(0n), issuer: ZERO } : headers[i];
|
|
58
|
+
});
|
|
59
|
+
return { client: { getContractEvents, readContract } as unknown as PublicClient, getContractEvents };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe('registry reads', () => {
|
|
63
|
+
it('reads the inbox with one getLogs per distinct block, in sharesFor order', async () => {
|
|
64
|
+
const { client, getContractEvents } = clientWith();
|
|
65
|
+
const { posted, unreadable } = await readSharesFor(client, REGISTRY, RECIPIENT);
|
|
66
|
+
|
|
67
|
+
expect(unreadable).toEqual([]);
|
|
68
|
+
expect(posted.map((p) => p.shareId)).toEqual(ids);
|
|
69
|
+
expect(posted.map((p) => p.share)).toEqual(shares);
|
|
70
|
+
expect(posted.map((p) => p.metadata)).toEqual([...metadata]);
|
|
71
|
+
expect(getContractEvents).toHaveBeenCalledTimes(2);
|
|
72
|
+
expect(getContractEvents).toHaveBeenCalledWith(
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
eventName: 'Shared',
|
|
75
|
+
fromBlock: 10n,
|
|
76
|
+
toBlock: 10n,
|
|
77
|
+
args: { shareId: [ids[0], ids[1]] },
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('reads one share, and null for an unknown one', async () => {
|
|
83
|
+
const { client } = clientWith();
|
|
84
|
+
expect((await readShare(client, REGISTRY, ids[2]))?.metadata).toBe('0x03bb');
|
|
85
|
+
expect(await readShare(client, REGISTRY, `0x${'f'.repeat(64)}`)).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('takes the last Shared event when a share was removed and posted again in the same block', async () => {
|
|
89
|
+
const { client } = clientWith([
|
|
90
|
+
{ block: 10n, acp: shares[0], id: ids[0], metadata: '0x03aa' },
|
|
91
|
+
{ block: 10n, acp: shares[0], id: ids[0], metadata: '0x03cc' },
|
|
92
|
+
]);
|
|
93
|
+
const {
|
|
94
|
+
posted: [posted],
|
|
95
|
+
} = await readPostedShares(client, REGISTRY, [{ shareId: ids[0], header: headers[0] }]);
|
|
96
|
+
expect(posted.metadata).toBe('0x03cc');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('one share never hides the rest', () => {
|
|
100
|
+
const all = () => shares.map((_, i) => ({ shareId: ids[i], header: headers[i] }));
|
|
101
|
+
|
|
102
|
+
it('a share whose block has no Shared event of it is unreadable, the others are posted', async () => {
|
|
103
|
+
// the node answered sharesFor from a newer block than the one it serves logs for
|
|
104
|
+
const { client } = clientWith(
|
|
105
|
+
shares.slice(0, 2).map((acp, i) => ({ block: headers[i].blockNumber, acp, id: ids[i], metadata: metadata[i] }))
|
|
106
|
+
);
|
|
107
|
+
const { posted, unreadable } = await readPostedShares(client, REGISTRY, all());
|
|
108
|
+
expect(posted.map((p) => p.shareId)).toEqual([ids[0], ids[1]]);
|
|
109
|
+
expect(unreadable).toEqual([
|
|
110
|
+
{ shareId: ids[2], header: headers[2], reason: 'no Shared event in block 20, where the registry points' },
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('a share whose event payload does not hash to its id is unreadable, the others are posted', async () => {
|
|
115
|
+
const { client } = clientWith([
|
|
116
|
+
{ block: 10n, acp: shares[1], id: ids[0], metadata: '0x' }, // the payload of share 2 under the id of share 1
|
|
117
|
+
{ block: 10n, acp: shares[1], id: ids[1], metadata: '0x' },
|
|
118
|
+
{ block: 20n, acp: shares[2], id: ids[2], metadata: '0x03bb' },
|
|
119
|
+
]);
|
|
120
|
+
const { posted, unreadable } = await readPostedShares(client, REGISTRY, all());
|
|
121
|
+
expect(posted.map((p) => p.shareId)).toEqual([ids[1], ids[2]]);
|
|
122
|
+
expect(unreadable.map((u) => [u.shareId, u.reason])).toEqual([
|
|
123
|
+
[ids[0], 'the payload in its Shared event does not hash to its id'],
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('a block the node cannot serve makes its shares unreadable, with the error, and the others are posted', async () => {
|
|
128
|
+
const { client, getContractEvents } = clientWith();
|
|
129
|
+
getContractEvents.mockImplementationOnce(async () => {
|
|
130
|
+
throw new Error('block not found');
|
|
131
|
+
});
|
|
132
|
+
const { posted, unreadable } = await readPostedShares(client, REGISTRY, all());
|
|
133
|
+
expect(posted.length + unreadable.length).toBe(3);
|
|
134
|
+
expect(unreadable.length).toBeGreaterThan(0);
|
|
135
|
+
for (const u of unreadable)
|
|
136
|
+
expect(u.reason).toMatch(/Shared events of block \d+ could not be read: block not found/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('readShare throws, with the reason, for a listed share whose event cannot be read', async () => {
|
|
140
|
+
const { client } = clientWith([]);
|
|
141
|
+
await expect(readShare(client, REGISTRY, ids[0])).rejects.toThrow('no Shared event in block 10');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { GenerateSealingKey, seal, unsealWithPrivateKey } from '../index.js';
|
|
3
|
+
import { CofheError, CofheErrorCode } from '../../core/error.js';
|
|
3
4
|
|
|
4
5
|
describe('seal / unsealWithPrivateKey', () => {
|
|
5
6
|
it('should seal data into EthEncryptedData shape', () => {
|
|
@@ -34,6 +35,14 @@ describe('seal / unsealWithPrivateKey', () => {
|
|
|
34
35
|
expect(unsealWithPrivateKey(pair.privateKey.slice(2), sealed)).toBe(value);
|
|
35
36
|
});
|
|
36
37
|
|
|
38
|
+
it('accepts uppercase hex keys', () => {
|
|
39
|
+
const pair = GenerateSealingKey();
|
|
40
|
+
const value = BigInt(7);
|
|
41
|
+
|
|
42
|
+
const sealed = seal(value, `0x${pair.publicKey.slice(2).toUpperCase()}`);
|
|
43
|
+
expect(unsealWithPrivateKey(`0x${pair.privateKey.slice(2).toUpperCase()}`, sealed)).toBe(value);
|
|
44
|
+
});
|
|
45
|
+
|
|
37
46
|
it('should throw error for invalid public key in seal', () => {
|
|
38
47
|
expect(() => {
|
|
39
48
|
seal(BigInt(12345), 'invalid');
|
|
@@ -49,6 +58,21 @@ describe('seal / unsealWithPrivateKey', () => {
|
|
|
49
58
|
}).toThrow('Private key must be of length 64');
|
|
50
59
|
});
|
|
51
60
|
|
|
61
|
+
it('rejects a correct-length public key with non-hex characters instead of sealing to a zeroed key', () => {
|
|
62
|
+
expect(() => {
|
|
63
|
+
seal(BigInt(12345), 'z'.repeat(64));
|
|
64
|
+
}).toThrow('Public key must contain only hex characters');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('rejects a correct-length private key with non-hex characters in unseal', () => {
|
|
68
|
+
const pair = GenerateSealingKey();
|
|
69
|
+
const sealed = seal(BigInt(1), pair.publicKey);
|
|
70
|
+
|
|
71
|
+
expect(() => {
|
|
72
|
+
unsealWithPrivateKey(`0x${'z'.repeat(64)}`, sealed);
|
|
73
|
+
}).toThrow('Private key must contain only hex characters');
|
|
74
|
+
});
|
|
75
|
+
|
|
52
76
|
it('should throw error for invalid value in seal', () => {
|
|
53
77
|
const publicKey = `0x${'b'.repeat(64)}`;
|
|
54
78
|
|
|
@@ -57,6 +81,56 @@ describe('seal / unsealWithPrivateKey', () => {
|
|
|
57
81
|
seal('not a number', publicKey);
|
|
58
82
|
}).toThrow('Value not a number is not a number or bigint: string');
|
|
59
83
|
});
|
|
84
|
+
|
|
85
|
+
describe('unseal failures are CofheErrors', () => {
|
|
86
|
+
const expectUnsealFailed = (fn: () => unknown, secrets: string[] = []) => {
|
|
87
|
+
let thrown: unknown;
|
|
88
|
+
try {
|
|
89
|
+
fn();
|
|
90
|
+
} catch (e) {
|
|
91
|
+
thrown = e;
|
|
92
|
+
}
|
|
93
|
+
expect(thrown).toBeInstanceOf(CofheError);
|
|
94
|
+
expect((thrown as CofheError).code).toBe(CofheErrorCode.SealOutputFailed);
|
|
95
|
+
const serialized = (thrown as CofheError).serialize();
|
|
96
|
+
for (const secret of secrets) expect(serialized).not.toContain(secret.replace(/^0x/, ''));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
it('throws SealOutputFailed when unsealing with the wrong private key', () => {
|
|
100
|
+
const pair = GenerateSealingKey();
|
|
101
|
+
const other = GenerateSealingKey();
|
|
102
|
+
const sealed = seal(BigInt(42), pair.publicKey);
|
|
103
|
+
|
|
104
|
+
expectUnsealFailed(() => unsealWithPrivateKey(other.privateKey, sealed), [other.privateKey, pair.privateKey]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('throws SealOutputFailed when the sealed payload is corrupted', () => {
|
|
108
|
+
const pair = GenerateSealingKey();
|
|
109
|
+
const sealed = seal(BigInt(42), pair.publicKey);
|
|
110
|
+
const corrupted = { ...sealed, data: Uint8Array.from(sealed.data) };
|
|
111
|
+
corrupted.data[0] ^= 0xff;
|
|
112
|
+
|
|
113
|
+
expectUnsealFailed(() => unsealWithPrivateKey(pair.privateKey, corrupted), [pair.privateKey]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('throws SealOutputFailed, keeping the cause, when nacl rejects malformed inputs', () => {
|
|
117
|
+
const pair = GenerateSealingKey();
|
|
118
|
+
const sealed = seal(BigInt(42), pair.publicKey);
|
|
119
|
+
const shortNonce = { ...sealed, nonce: sealed.nonce.slice(0, 4) };
|
|
120
|
+
|
|
121
|
+
expectUnsealFailed(() => unsealWithPrivateKey(pair.privateKey, shortNonce), [pair.privateKey]);
|
|
122
|
+
try {
|
|
123
|
+
unsealWithPrivateKey(pair.privateKey, shortNonce);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
expect((e as CofheError).cause).toBeInstanceOf(Error);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('still reports a malformed sealing key as a plain key error, not a seal failure', () => {
|
|
130
|
+
const sealed = seal(BigInt(1), GenerateSealingKey().publicKey);
|
|
131
|
+
expect(() => unsealWithPrivateKey('deadbeef', sealed)).toThrow('Private key must be of length 64');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
60
134
|
});
|
|
61
135
|
|
|
62
136
|
describe('GenerateSealingKey', () => {
|
package/acps/test/store.test.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
removeACP,
|
|
13
13
|
getActiveACPHash,
|
|
14
14
|
setActiveACPHash,
|
|
15
|
+
clearStaleStore,
|
|
16
|
+
ACP_STORE_DEFAULTS,
|
|
15
17
|
ACPUtils,
|
|
16
18
|
} from '../index.js';
|
|
17
19
|
|
|
@@ -85,4 +87,73 @@ describe('Storage Tests', () => {
|
|
|
85
87
|
expect(activeHash).toBeUndefined();
|
|
86
88
|
});
|
|
87
89
|
});
|
|
90
|
+
|
|
91
|
+
describe('Stale Store Detection', () => {
|
|
92
|
+
// `typeof null` and `typeof []` are both 'object', so these shapes used to pass the
|
|
93
|
+
// structure guard and stay in the store; a `null` record then throws on `acps[chainId]`.
|
|
94
|
+
const seedRawState = (state: unknown) => {
|
|
95
|
+
acpStore.store.setState(state as never, true);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
it('should clear the store when acps is null', () => {
|
|
99
|
+
seedRawState({ acps: null, activeACPHash: {} });
|
|
100
|
+
|
|
101
|
+
clearStaleStore();
|
|
102
|
+
|
|
103
|
+
expect(acpStore.store.getState()).toEqual(ACP_STORE_DEFAULTS);
|
|
104
|
+
expect(() => getACP(chainId, account, '0xdeadbeef')).not.toThrow();
|
|
105
|
+
expect(getACP(chainId, account, '0xdeadbeef')).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should clear the store when acps is an array', () => {
|
|
109
|
+
seedRawState({ acps: [], activeACPHash: {} });
|
|
110
|
+
|
|
111
|
+
clearStaleStore();
|
|
112
|
+
|
|
113
|
+
expect(acpStore.store.getState()).toEqual(ACP_STORE_DEFAULTS);
|
|
114
|
+
expect(() => getACP(chainId, account, '0xdeadbeef')).not.toThrow();
|
|
115
|
+
expect(getACP(chainId, account, '0xdeadbeef')).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should clear the store when activeACPHash is null', () => {
|
|
119
|
+
seedRawState({ acps: {}, activeACPHash: null });
|
|
120
|
+
|
|
121
|
+
clearStaleStore();
|
|
122
|
+
|
|
123
|
+
expect(acpStore.store.getState()).toEqual(ACP_STORE_DEFAULTS);
|
|
124
|
+
expect(() => getActiveACP(chainId, account)).not.toThrow();
|
|
125
|
+
expect(getActiveACP(chainId, account)).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should clear the store when activeACPHash is an array', () => {
|
|
129
|
+
seedRawState({ acps: {}, activeACPHash: [] });
|
|
130
|
+
|
|
131
|
+
clearStaleStore();
|
|
132
|
+
|
|
133
|
+
expect(acpStore.store.getState()).toEqual(ACP_STORE_DEFAULTS);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should let setACP recover a store whose acps is null', async () => {
|
|
137
|
+
const acp = await createMockACP();
|
|
138
|
+
seedRawState({ acps: null, activeACPHash: {} });
|
|
139
|
+
|
|
140
|
+
expect(() => setACP(chainId, account, acp)).not.toThrow();
|
|
141
|
+
expect(ACPUtils.serialize(getACP(chainId, account, acp.hash)!)).toEqual(ACPUtils.serialize(acp));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should leave a valid store untouched', async () => {
|
|
145
|
+
const acp = await createMockACP();
|
|
146
|
+
setACP(chainId, account, acp);
|
|
147
|
+
setActiveACPHash(chainId, account, acp.hash);
|
|
148
|
+
|
|
149
|
+
const before = acpStore.store.getState();
|
|
150
|
+
|
|
151
|
+
clearStaleStore();
|
|
152
|
+
|
|
153
|
+
// Same reference: a valid store must not be rewritten at all.
|
|
154
|
+
expect(acpStore.store.getState()).toBe(before);
|
|
155
|
+
expect(ACPUtils.serialize(getACP(chainId, account, acp.hash)!)).toEqual(ACPUtils.serialize(acp));
|
|
156
|
+
expect(getActiveACPHash(chainId, account)).toBe(acp.hash);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
88
159
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { fromHexString, toBeArray, toHexString } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
describe('fromHexString', () => {
|
|
5
|
+
it('strips the 0x prefix before padding an odd-length string', () => {
|
|
6
|
+
// Regression: padding first produced `00x123`, which decoded to [0x00, NaN->0, 0x23].
|
|
7
|
+
expect(Array.from(fromHexString('0x123'))).toEqual([0x01, 0x23]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('pads odd-length strings without a prefix', () => {
|
|
11
|
+
expect(Array.from(fromHexString('123'))).toEqual([0x01, 0x23]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('decodes even-length strings with and without the 0x prefix identically', () => {
|
|
15
|
+
expect(Array.from(fromHexString('0x1234'))).toEqual([0x12, 0x34]);
|
|
16
|
+
expect(Array.from(fromHexString('1234'))).toEqual([0x12, 0x34]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('decodes uppercase hex', () => {
|
|
20
|
+
expect(Array.from(fromHexString('0xDEADBEEF'))).toEqual([0xde, 0xad, 0xbe, 0xef]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns an empty array for empty input', () => {
|
|
24
|
+
expect(fromHexString('')).toEqual(new Uint8Array());
|
|
25
|
+
expect(fromHexString('0x')).toEqual(new Uint8Array());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('round-trips a 32-byte sealing key through toHexString', () => {
|
|
29
|
+
const privateKey = 'a3'.repeat(32);
|
|
30
|
+
|
|
31
|
+
const bytes = fromHexString(privateKey);
|
|
32
|
+
|
|
33
|
+
expect(bytes.length).toBe(32);
|
|
34
|
+
expect(toHexString(bytes)).toBe(privateKey);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('throws instead of silently producing zero bytes for non-hex characters', () => {
|
|
38
|
+
expect(() => fromHexString('zzzz')).toThrow('Invalid hex string');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('throws for hex strings with a single invalid character', () => {
|
|
42
|
+
expect(() => fromHexString('0x12g4')).toThrow('Invalid hex string');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('only strips a lowercase 0x prefix, matching the sealing key and ACP schema checks', () => {
|
|
46
|
+
expect(() => fromHexString('0X12')).toThrow('Invalid hex string');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not leak the value in the error message', () => {
|
|
50
|
+
const secret = 'f'.repeat(63) + 'z';
|
|
51
|
+
|
|
52
|
+
expect(() => fromHexString(secret)).toThrow(
|
|
53
|
+
expect.objectContaining({ message: expect.not.stringContaining(secret) })
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('toBeArray', () => {
|
|
59
|
+
it('still encodes values that need an odd-length hex representation', () => {
|
|
60
|
+
expect(Array.from(toBeArray(0x123))).toEqual([0x01, 0x23]);
|
|
61
|
+
expect(Array.from(toBeArray(255n))).toEqual([0xff]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -91,7 +91,6 @@ describe('Validation Tests', () => {
|
|
|
91
91
|
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
92
92
|
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Alice's address
|
|
93
93
|
issuerSignature: '0x1234567890abcdef',
|
|
94
|
-
name: 'Import ACP',
|
|
95
94
|
};
|
|
96
95
|
|
|
97
96
|
expect(() => validateImportACPOptions(options)).not.toThrow();
|
|
@@ -102,7 +101,6 @@ describe('Validation Tests', () => {
|
|
|
102
101
|
issuer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // Bob's address
|
|
103
102
|
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Alice's address
|
|
104
103
|
issuerSignature: '0x1234567890abcdef',
|
|
105
|
-
name: 'Import ACP',
|
|
106
104
|
};
|
|
107
105
|
expect(() => validateImportACPOptions(options)).toThrow();
|
|
108
106
|
});
|
|
@@ -113,7 +111,6 @@ describe('Validation Tests', () => {
|
|
|
113
111
|
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
114
112
|
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Alice's address
|
|
115
113
|
issuerSignature: '0x',
|
|
116
|
-
name: 'Import ACP',
|
|
117
114
|
};
|
|
118
115
|
|
|
119
116
|
expect(() => validateImportACPOptions(options)).toThrow();
|
|
@@ -125,7 +122,6 @@ describe('Validation Tests', () => {
|
|
|
125
122
|
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
126
123
|
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Alice's address
|
|
127
124
|
issuerSignature: '0x',
|
|
128
|
-
name: 'Import ACP',
|
|
129
125
|
};
|
|
130
126
|
|
|
131
127
|
expect(() => validateImportACPOptions(options)).toThrow();
|