@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
@@ -0,0 +1,157 @@
1
+ import type { Address, Hex } from 'viem';
2
+ import type { ShareLabel } from '../schema';
3
+
4
+ /**
5
+ * Shared test fixtures: two confidential tokens on Arbitrum Sepolia, labels with the exact blob
6
+ * each encodes to, and one real share whose labels the chain confirms.
7
+ */
8
+
9
+ export const FUSD: Address = '0x389be1Eb842290F264A6851f49c0149b8035a052';
10
+ export const FUSDY: Address = '0x06a52cf2e0cEAFc7d1f522400500ca7f3df18754';
11
+ export const ISSUER: Address = '0x1F547C0A6088856B9d58F843d9dd8C6e26f4969d';
12
+ /** confidentialBalanceOf(address) */
13
+ export const BALANCE_OF: Hex = '0x344ff101';
14
+ /** ConfidentialTransfer(address indexed from, address indexed to, bytes32 indexed amount), first 4 bytes of topic0 */
15
+ export const CONFIDENTIAL_TRANSFER: Hex = '0x67500e8d';
16
+ export const CONFIDENTIAL_TRANSFER_TOPIC0: Hex = '0x67500e8d0ed826d2194f514dd0d8124f35648ab6e3fb5e6ed867134cffe661e9';
17
+ export const WORD: Hex = '0x00000000000000000000000000000000000000000000000000000000000000aa';
18
+
19
+ const fusd = FUSD.slice(2).toLowerCase();
20
+ const fusdy = FUSDY.slice(2).toLowerCase();
21
+
22
+ /** Label lists with the blob each must encode to, written field by field. */
23
+ export const CASES: { name: string; labels: ShareLabel[]; blob: string }[] = [
24
+ {
25
+ name: 'a stored value read with the issuer as its one argument',
26
+ labels: [
27
+ {
28
+ kind: 'stored',
29
+ contract: FUSD,
30
+ selector: BALANCE_OF,
31
+ args: [{ type: 'issuer' }],
32
+ returnWord: 0,
33
+ block: 0x12ba88b2n,
34
+ },
35
+ ],
36
+ blob: '0x03' + '01' + fusd + '01' + '344ff101' + '00' + ('00' + '00' + '00' + '12ba88b2' + '01'),
37
+ },
38
+ {
39
+ name: 'a stored value read with no arguments',
40
+ labels: [{ kind: 'stored', contract: FUSD, selector: '0x18160ddd', args: [], returnWord: 0, block: 1n }],
41
+ blob: '0x03' + '01' + fusd + '01' + '18160ddd' + '00' + ('00' + '00' + '00' + '00000001' + '00'),
42
+ },
43
+ {
44
+ name: 'a stored value with explicit arguments and a later return word',
45
+ labels: [
46
+ {
47
+ kind: 'stored',
48
+ contract: FUSDY,
49
+ selector: '0x70a08231',
50
+ args: [{ type: 'issuer' }, { type: 'word', value: WORD }],
51
+ returnWord: 1,
52
+ block: 0x12ba88b2n,
53
+ },
54
+ ],
55
+ blob:
56
+ '0x03' +
57
+ '01' +
58
+ fusdy +
59
+ '01' +
60
+ '70a08231' +
61
+ '00' +
62
+ ('00' + '00' + '00' + '12ba88b2' + 'ff' + '01' + '02' + '01' + '00' + WORD.slice(2)),
63
+ },
64
+ {
65
+ name: 'an event value',
66
+ labels: [
67
+ { kind: 'event', contract: FUSD, block: 0x12ba9304n, txIndex: 7, logIndex: 300, selector: CONFIDENTIAL_TRANSFER },
68
+ ],
69
+ blob: '0x03' + '01' + fusd + '01' + '67500e8d' + '01' + '12ba9304' + '0007' + ('01' + '00' + '00' + '012c' + '00'),
70
+ },
71
+ {
72
+ name: 'two event values of one transaction on two contracts',
73
+ labels: [
74
+ {
75
+ kind: 'event',
76
+ contract: FUSD,
77
+ block: 0x12ba9375n,
78
+ txIndex: 12,
79
+ logIndex: 106,
80
+ selector: CONFIDENTIAL_TRANSFER,
81
+ },
82
+ {
83
+ kind: 'event',
84
+ contract: FUSDY,
85
+ block: 0x12ba9375n,
86
+ txIndex: 12,
87
+ logIndex: 115,
88
+ selector: CONFIDENTIAL_TRANSFER,
89
+ },
90
+ ],
91
+ blob:
92
+ '0x03' +
93
+ ('02' + fusd + fusdy) +
94
+ ('01' + '67500e8d') +
95
+ ('01' + '12ba9375' + '000c') +
96
+ ('01' + '00' + '00' + '006a' + '00') +
97
+ ('01' + '01' + '00' + '0073' + '00'),
98
+ },
99
+ {
100
+ name: 'an unlabelled value',
101
+ labels: [{ kind: 'unlabelled' }],
102
+ blob: '0x03' + '00' + '00' + '00' + 'ff',
103
+ },
104
+ ];
105
+
106
+ /**
107
+ * A real share on Arbitrum Sepolia: two balances read at one block, and five transfer amounts from
108
+ * four transactions. The last two are the two values of one vault deposit (the fUSD that left and
109
+ * the fUSDy that was minted), so they name the same transaction. The fUSDy amount is carried by two
110
+ * logs of that transaction (115, the ConfidentialTransfer, and 118); the label names the first.
111
+ */
112
+ export const SAMPLE = {
113
+ issuer: ISSUER,
114
+ blob: ('0x03' +
115
+ ('02' + fusd + fusdy) +
116
+ ('02' + '344ff101' + '67500e8d') +
117
+ ('04' + '12ba93040005' + '12ba93070008' + '12ba9309000a' + '12ba9375000c') +
118
+ '00000012bb9d0f01' +
119
+ '00010012bb9d0f01' +
120
+ '010000001e01' +
121
+ '010001004101' +
122
+ '010002008a01' +
123
+ '010003006a01' +
124
+ '010103007301') as Hex,
125
+ handles: [
126
+ '0xabaaf65246a7b1763ed4f04486115a27ffbd7abac4a0d10f139cfdac9a910500',
127
+ '0x987a1e51721082556753153555c4f2242a8a592d80ff9551fb471f7427660500',
128
+ '0xbb0c6b1feb27e5c2aec9667ce478006db331205407c754c7a47befa4b2c28500',
129
+ '0x409fec2a564efb61ad75d544ca1933fe2d450e5d9ba847089b15375d1c0d8500',
130
+ '0x39a2c1973989ae9e52dc4d0eed55ceb761d8b83dae6c1a8a66aa9d1defda8500',
131
+ '0xf726d299727dd5c150ce1f31f42447495b3f4f42204d043ab9d21e78aced0500',
132
+ '0x9b0420892380838a0d1fc858c0c2314f9248adb4cacaa766fe869645c1830500',
133
+ ] as Hex[],
134
+ labels: [
135
+ {
136
+ kind: 'stored',
137
+ contract: FUSD,
138
+ selector: BALANCE_OF,
139
+ args: [{ type: 'issuer' }],
140
+ returnWord: 0,
141
+ block: 314285327n,
142
+ },
143
+ {
144
+ kind: 'stored',
145
+ contract: FUSDY,
146
+ selector: BALANCE_OF,
147
+ args: [{ type: 'issuer' }],
148
+ returnWord: 0,
149
+ block: 314285327n,
150
+ },
151
+ { kind: 'event', contract: FUSD, block: 314217220n, txIndex: 5, logIndex: 30, selector: CONFIDENTIAL_TRANSFER },
152
+ { kind: 'event', contract: FUSD, block: 314217223n, txIndex: 8, logIndex: 65, selector: CONFIDENTIAL_TRANSFER },
153
+ { kind: 'event', contract: FUSD, block: 314217225n, txIndex: 10, logIndex: 138, selector: CONFIDENTIAL_TRANSFER },
154
+ { kind: 'event', contract: FUSD, block: 314217333n, txIndex: 12, logIndex: 106, selector: CONFIDENTIAL_TRANSFER },
155
+ { kind: 'event', contract: FUSDY, block: 314217333n, txIndex: 12, logIndex: 115, selector: CONFIDENTIAL_TRANSFER },
156
+ ] as ShareLabel[],
157
+ };
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createPublicClient, http, type PublicClient } from 'viem';
3
+ import { arbitrumSepolia } from 'viem/chains';
4
+ import { TEST_PRIVATE_KEY } from '@cofhe/test-setup';
5
+ import { decodeShareMetadata } from '../decode';
6
+ import { verifyShareLabels } from '../verify';
7
+ import { SAMPLE } from './fixtures';
8
+
9
+ /**
10
+ * The real sample against Arbitrum Sepolia, read from the public RPC. A live-chain suite like
11
+ * `core/test/decrypt.test.ts`: it runs where TEST_PRIVATE_KEY is set (the key itself is not used),
12
+ * so `pnpm test` stays offline and `pnpm test:live` covers it.
13
+ */
14
+ describe.skipIf(!TEST_PRIVATE_KEY)('share metadata against Arbitrum Sepolia', () => {
15
+ const publicClient = createPublicClient({
16
+ chain: arbitrumSepolia,
17
+ transport: http(arbitrumSepolia.rpcUrls.default.http[0]),
18
+ }) as PublicClient;
19
+
20
+ it('confirms every event label of the sample at its log index', async () => {
21
+ const labels = decodeShareMetadata(SAMPLE.blob, SAMPLE.handles);
22
+ const checks = await verifyShareLabels({
23
+ labels,
24
+ handles: SAMPLE.handles,
25
+ issuer: SAMPLE.issuer,
26
+ publicClient,
27
+ verify: 'events',
28
+ });
29
+ expect(checks).toEqual(['skipped', 'skipped', 'verified', 'verified', 'verified', 'verified', 'verified']);
30
+ });
31
+
32
+ it('rejects the fUSDy amount labelled at the deposit’s other log that carries it', async () => {
33
+ const labels = decodeShareMetadata(SAMPLE.blob, SAMPLE.handles);
34
+ const last = labels[6];
35
+ if (last.kind !== 'event') throw new Error('expected an event label');
36
+ const [check] = await verifyShareLabels({
37
+ labels: [{ ...last, logIndex: 118 }],
38
+ handles: [SAMPLE.handles[6]],
39
+ issuer: SAMPLE.issuer,
40
+ publicClient,
41
+ });
42
+ // log 118 carries the ctHash but is not a ConfidentialTransfer: the selector check fails it
43
+ expect(check).toBe('mismatch');
44
+ });
45
+
46
+ it('checks the stored values while a node still has the state of their block', async () => {
47
+ const labels = decodeShareMetadata(SAMPLE.blob, SAMPLE.handles);
48
+ const checks = await verifyShareLabels({
49
+ labels: labels.slice(0, 2),
50
+ handles: SAMPLE.handles.slice(0, 2),
51
+ issuer: SAMPLE.issuer,
52
+ publicClient,
53
+ });
54
+ for (const check of checks) expect(['verified', 'unverifiable']).toContain(check);
55
+ });
56
+ });
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { padHex, parseAbi, toEventSelector, toFunctionSelector, type Log } from 'viem';
3
+ import { encodeShareMetadata } from '../encode';
4
+ import { ERC7984_ABI } from '../erc7984';
5
+ import { eventLogLabel, viewFunctionLabel } from '../presets';
6
+ import {
7
+ BALANCE_OF,
8
+ CONFIDENTIAL_TRANSFER,
9
+ CONFIDENTIAL_TRANSFER_TOPIC0,
10
+ FUSD,
11
+ FUSDY,
12
+ ISSUER,
13
+ SAMPLE,
14
+ WORD,
15
+ } from './fixtures';
16
+
17
+ const ISSUER_WORD = padHex(ISSUER, { size: 32 }).toLowerCase();
18
+
19
+ describe('label presets', () => {
20
+ it('labels a log with its position and event selector', () => {
21
+ const log = {
22
+ address: FUSD,
23
+ blockNumber: 314217220n,
24
+ transactionIndex: 5,
25
+ logIndex: 30,
26
+ topics: [CONFIDENTIAL_TRANSFER_TOPIC0],
27
+ } as unknown as Log;
28
+ expect(eventLogLabel(log)).toEqual(SAMPLE.labels[2]);
29
+ expect(eventLogLabel(log).selector).toBe(CONFIDENTIAL_TRANSFER);
30
+ });
31
+
32
+ it('refuses a pending or anonymous log', () => {
33
+ const base = {
34
+ address: FUSD,
35
+ blockNumber: 1n,
36
+ transactionIndex: 0,
37
+ logIndex: 0,
38
+ topics: [CONFIDENTIAL_TRANSFER_TOPIC0],
39
+ };
40
+ expect(() => eventLogLabel({ ...base, blockNumber: null } as unknown as Log)).toThrow('pending');
41
+ expect(() => eventLogLabel({ ...base, topics: [] } as unknown as Log)).toThrow('anonymous');
42
+ });
43
+
44
+ it('labels the ERC-7984 balance read from its ABI; the issuer argument compresses to the real share', () => {
45
+ const [balanceOf, transfer] = ERC7984_ABI;
46
+ expect(toFunctionSelector(balanceOf)).toBe(BALANCE_OF);
47
+ expect(toEventSelector(transfer)).toBe(CONFIDENTIAL_TRANSFER_TOPIC0);
48
+
49
+ const label = viewFunctionLabel({
50
+ contract: FUSD,
51
+ abi: ERC7984_ABI,
52
+ functionName: 'confidentialBalanceOf',
53
+ args: [ISSUER],
54
+ block: 314285327n,
55
+ });
56
+ expect(label).toEqual({ ...SAMPLE.labels[0], args: [{ type: 'word', value: ISSUER_WORD }] });
57
+ // encoded for a share by ISSUER, the address becomes "the issuer": the sample's own bytes
58
+ expect(encodeShareMetadata([label], { issuer: ISSUER })).toBe(encodeShareMetadata([SAMPLE.labels[0]]));
59
+ // for anyone else's share it stays a literal address
60
+ expect(encodeShareMetadata([label], { issuer: FUSDY })).toBe(encodeShareMetadata([label]));
61
+ expect(encodeShareMetadata([label])).not.toBe(encodeShareMetadata([SAMPLE.labels[0]]));
62
+ });
63
+
64
+ it('labels any view function from its ABI, encoding literal arguments to words', () => {
65
+ const abi = parseAbi([
66
+ 'function balanceOfAt(address account, uint256 snapshot) view returns (uint256 total, bytes32 encrypted)',
67
+ 'function totalSupply() view returns (bytes32)',
68
+ ]);
69
+ expect(
70
+ viewFunctionLabel({
71
+ contract: FUSDY,
72
+ abi,
73
+ functionName: 'balanceOfAt',
74
+ args: [ISSUER, 0xaan],
75
+ returnWord: 1,
76
+ block: 0x12ba88b2n,
77
+ })
78
+ ).toEqual({
79
+ kind: 'stored',
80
+ contract: FUSDY,
81
+ selector: toFunctionSelector(abi[0]),
82
+ args: [
83
+ { type: 'word', value: ISSUER_WORD },
84
+ { type: 'word', value: WORD },
85
+ ],
86
+ returnWord: 1,
87
+ block: 0x12ba88b2n,
88
+ });
89
+ expect(viewFunctionLabel({ contract: FUSD, abi, functionName: 'totalSupply', block: 1n })).toEqual({
90
+ kind: 'stored',
91
+ contract: FUSD,
92
+ selector: '0x18160ddd',
93
+ args: [],
94
+ returnWord: 0,
95
+ block: 1n,
96
+ });
97
+ });
98
+
99
+ it('refuses what the layout cannot carry or the ABI does not have', () => {
100
+ const abi = parseAbi([
101
+ 'function nameOf(string key) view returns (bytes32)',
102
+ 'function balanceOf(address account) view returns (bytes32)',
103
+ 'function balanceOf(address account, uint256 id) view returns (bytes32)',
104
+ 'function mint(address to) returns (bytes32)',
105
+ ]);
106
+ const base = { contract: FUSD, abi, block: 1n } as const;
107
+ expect(() => viewFunctionLabel({ ...base, functionName: 'nameOf', args: ['x'] })).toThrow('static 32-byte word');
108
+ expect(() => viewFunctionLabel({ ...base, functionName: 'balanceOf', args: [] as never })).toThrow(
109
+ 'no single overload'
110
+ );
111
+ expect(() =>
112
+ viewFunctionLabel({ ...base, abi: parseAbi(['function f(address a) view returns (bytes32)']), functionName: 'f' })
113
+ ).toThrow('takes 1 arguments, 0 given');
114
+ expect(() => viewFunctionLabel({ ...base, functionName: 'missing' as never })).toThrow('no function missing');
115
+ expect(() => viewFunctionLabel({ ...base, functionName: 'mint' as never, args: [ISSUER] as never })).toThrow(
116
+ 'not a view function'
117
+ );
118
+ });
119
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { concatHex, padHex, type Hex, type Log, type PublicClient } from 'viem';
3
+ import { verifyShareLabel, verifyShareLabels } from '../verify';
4
+ import type { ShareLabel } from '../schema';
5
+ import { BALANCE_OF, CONFIDENTIAL_TRANSFER, CONFIDENTIAL_TRANSFER_TOPIC0, FUSD, FUSDY, ISSUER } from './fixtures';
6
+
7
+ const AMOUNT = `0x${'11'.repeat(32)}` as Hex;
8
+ const BALANCE = `0x${'22'.repeat(32)}` as Hex;
9
+ const OTHER = `0x${'33'.repeat(32)}` as Hex;
10
+ const ALICE = padHex('0xa11ce', { size: 32 });
11
+
12
+ const transferLog = (overrides: Partial<Log> = {}): Log =>
13
+ ({
14
+ address: FUSD,
15
+ blockNumber: 100n,
16
+ transactionIndex: 3,
17
+ logIndex: 9,
18
+ topics: [CONFIDENTIAL_TRANSFER_TOPIC0, ALICE, ALICE, AMOUNT],
19
+ data: '0x',
20
+ ...overrides,
21
+ }) as Log;
22
+
23
+ const eventLabel = (overrides: Partial<ShareLabel & { kind: 'event' }> = {}): ShareLabel => ({
24
+ kind: 'event',
25
+ contract: FUSD,
26
+ block: 100n,
27
+ txIndex: 3,
28
+ logIndex: 9,
29
+ selector: CONFIDENTIAL_TRANSFER,
30
+ ...overrides,
31
+ });
32
+
33
+ const balanceLabel: ShareLabel = {
34
+ kind: 'stored',
35
+ contract: FUSDY,
36
+ selector: BALANCE_OF,
37
+ args: [{ type: 'issuer' }],
38
+ returnWord: 0,
39
+ block: 90n,
40
+ };
41
+
42
+ const clientWith = ({ logs = [transferLog()], callResult = BALANCE }: { logs?: Log[]; callResult?: Hex } = {}) => {
43
+ const getLogs = vi.fn(async () => logs);
44
+ const call = vi.fn(async () => ({ data: callResult }));
45
+ return { client: { getLogs, call } as unknown as PublicClient, getLogs, call };
46
+ };
47
+
48
+ describe('verifyShareLabel', () => {
49
+ it('verifies an event value at its log index', async () => {
50
+ const { client, getLogs } = clientWith();
51
+ expect(await verifyShareLabel(eventLabel(), AMOUNT, client, ISSUER)).toBe('verified');
52
+ expect(getLogs).toHaveBeenCalledWith({ address: FUSD, fromBlock: 100n, toBlock: 100n });
53
+ });
54
+
55
+ it('finds the ctHash among the data words too', async () => {
56
+ const { client } = clientWith({
57
+ logs: [transferLog({ topics: [CONFIDENTIAL_TRANSFER_TOPIC0], data: concatHex([OTHER, AMOUNT]) })],
58
+ });
59
+ expect(await verifyShareLabel(eventLabel(), AMOUNT, client, ISSUER)).toBe('verified');
60
+ });
61
+
62
+ it.each([
63
+ ['another log index', eventLabel({ logIndex: 8 })],
64
+ ['another transaction', eventLabel({ txIndex: 4 })],
65
+ ['another event selector', eventLabel({ selector: '0xddf252ad' })],
66
+ ])('rejects an event value naming %s', async (_, label) => {
67
+ const { client } = clientWith();
68
+ expect(await verifyShareLabel(label, AMOUNT, client, ISSUER)).toBe('mismatch');
69
+ });
70
+
71
+ it('rejects an event value whose log does not carry the ctHash', async () => {
72
+ const { client } = clientWith();
73
+ expect(await verifyShareLabel(eventLabel(), OTHER, client, ISSUER)).toBe('mismatch');
74
+ });
75
+
76
+ it('verifies a stored value by repeating the call with the issuer at the labelled block', async () => {
77
+ const { client, call } = clientWith();
78
+ expect(await verifyShareLabel(balanceLabel, BALANCE, client, ISSUER)).toBe('verified');
79
+ expect(call).toHaveBeenCalledWith({
80
+ to: FUSDY,
81
+ data: concatHex([BALANCE_OF, padHex(ISSUER, { size: 32 })]),
82
+ blockNumber: 90n,
83
+ });
84
+ });
85
+
86
+ it('rejects a stored value the call does not return', async () => {
87
+ const { client } = clientWith({ callResult: OTHER });
88
+ expect(await verifyShareLabel(balanceLabel, BALANCE, client, ISSUER)).toBe('mismatch');
89
+ });
90
+
91
+ it('reports a failed request, and an unlabelled value, as unverifiable', async () => {
92
+ const client = { call: vi.fn().mockRejectedValue(new Error('missing trie node')) } as unknown as PublicClient;
93
+ expect(await verifyShareLabel(balanceLabel, BALANCE, client, ISSUER)).toBe('unverifiable');
94
+ expect(await verifyShareLabel({ kind: 'unlabelled' }, BALANCE, client, ISSUER)).toBe('unverifiable');
95
+ });
96
+ });
97
+
98
+ describe('verifyShareLabels', () => {
99
+ const labels: ShareLabel[] = [balanceLabel, eventLabel(), eventLabel({ logIndex: 10 }), { kind: 'unlabelled' }];
100
+ const handles = [BALANCE, AMOUNT, OTHER, OTHER];
101
+ const logs = [
102
+ transferLog(),
103
+ transferLog({ logIndex: 10, topics: [CONFIDENTIAL_TRANSFER_TOPIC0, ALICE, ALICE, OTHER] }),
104
+ ];
105
+
106
+ it("checks everything with 'all', one getLogs per contract and block", async () => {
107
+ const { client, getLogs, call } = clientWith({ logs });
108
+ expect(await verifyShareLabels({ labels, handles, issuer: ISSUER, publicClient: client })).toEqual([
109
+ 'verified',
110
+ 'verified',
111
+ 'verified',
112
+ 'unverifiable',
113
+ ]);
114
+ expect(getLogs).toHaveBeenCalledTimes(1);
115
+ expect(call).toHaveBeenCalledTimes(1);
116
+ });
117
+
118
+ it("skips stored values with 'events'", async () => {
119
+ const { client, call } = clientWith({ logs });
120
+ expect(
121
+ await verifyShareLabels({ labels, handles, issuer: ISSUER, publicClient: client, verify: 'events' })
122
+ ).toEqual(['skipped', 'verified', 'verified', 'unverifiable']);
123
+ expect(call).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it("makes no request with 'none'", async () => {
127
+ const { client, call, getLogs } = clientWith({ logs });
128
+ expect(await verifyShareLabels({ labels, handles, issuer: ISSUER, publicClient: client, verify: 'none' })).toEqual([
129
+ 'skipped',
130
+ 'skipped',
131
+ 'skipped',
132
+ 'skipped',
133
+ ]);
134
+ expect(call).not.toHaveBeenCalled();
135
+ expect(getLogs).not.toHaveBeenCalled();
136
+ });
137
+
138
+ it('refuses labels that do not match the handles one for one', async () => {
139
+ const { client } = clientWith();
140
+ await expect(
141
+ verifyShareLabels({ labels, handles: [BALANCE], issuer: ISSUER, publicClient: client })
142
+ ).rejects.toThrow('4 labels for 1 ctHashes');
143
+ });
144
+ });
@@ -0,0 +1,155 @@
1
+ import { concatHex, padHex, size, sliceHex, type Address, type Hex, type Log, type PublicClient } from 'viem';
2
+ import { BYTE_WIDTH } from './layout';
3
+ import type { EventValueLabel, ShareLabel, StoredValueLabel } from './schema.js';
4
+
5
+ /**
6
+ * - `verified`: the chain confirms the label.
7
+ * - `mismatch`: the chain contradicts it.
8
+ * - `unverifiable`: the chain could not be asked — an unlabelled ctHash, a node without the
9
+ * state of that block (a stored value needs an archive node once its block is old), or a
10
+ * failed request.
11
+ */
12
+ export type LabelVerification = 'verified' | 'mismatch' | 'unverifiable';
13
+
14
+ /**
15
+ * How much of a share's labels a reader checks against the chain. Labels are the issuer's claim:
16
+ * - `all` (default): every label — one `getLogs` per contract and block, one call per stored value;
17
+ * - `events`: event values only, which any node can answer however old the block;
18
+ * - `none`: trust the labels as written, no requests (the issuer previewing their own share).
19
+ */
20
+ export type ShareLabelVerifyMode = 'all' | 'events' | 'none';
21
+
22
+ /** A label's verification, or `skipped` when the verify mode leaves it unchecked. */
23
+ export type ShareLabelCheck = LabelVerification | 'skipped';
24
+
25
+ const sameWord = (a: Hex, b: Hex): boolean => a.toLowerCase() === b.toLowerCase();
26
+
27
+ /** The 32-byte words of a byte string, in order. A trailing partial word is left out. */
28
+ const wordsOf = (data: Hex): Hex[] => {
29
+ const count = Math.floor(size(data) / BYTE_WIDTH.word);
30
+ return Array.from({ length: count }, (_, index) =>
31
+ sliceHex(data, index * BYTE_WIDTH.word, (index + 1) * BYTE_WIDTH.word)
32
+ );
33
+ };
34
+
35
+ /** The view call a stored-value label names, with the issuer argument filled in. */
36
+ const storedCallData = (label: StoredValueLabel, issuer: Address): Hex =>
37
+ concatHex([
38
+ label.selector,
39
+ ...label.args.map((arg) => (arg.type === 'issuer' ? padHex(issuer, { size: BYTE_WIDTH.word }) : arg.value)),
40
+ ]);
41
+
42
+ /** Whether the ctHash is the labelled return word of the call's result. */
43
+ const checkStored = (label: StoredValueLabel, handle: Hex, data: Hex | undefined): LabelVerification => {
44
+ const found = data ? wordsOf(data)[label.returnWord] : undefined;
45
+ return found && sameWord(found, handle) ? 'verified' : 'mismatch';
46
+ };
47
+
48
+ /**
49
+ * Whether the labelled log is there and carries the ctHash: among the logs of the contract in the
50
+ * labelled block, the one at the labelled log index must belong to the labelled transaction, its
51
+ * topic0 must start with the labelled selector, and the ctHash must be one of its topics or data
52
+ * words.
53
+ */
54
+ const checkEvent = (label: EventValueLabel, handle: Hex, logs: readonly Log[]): LabelVerification => {
55
+ const log = logs.find((l) => l.logIndex === label.logIndex);
56
+ if (log == null) return 'mismatch';
57
+ if (log.transactionIndex !== label.txIndex) return 'mismatch';
58
+ const topic0 = log.topics[0];
59
+ if (topic0 == null || !topic0.toLowerCase().startsWith(label.selector.toLowerCase())) return 'mismatch';
60
+ const carried = [...log.topics.slice(1), ...wordsOf(log.data)].some((word) => word != null && sameWord(word, handle));
61
+ return carried ? 'verified' : 'mismatch';
62
+ };
63
+
64
+ const callAt = async (publicClient: PublicClient, to: Address, data: Hex, blockNumber: bigint) =>
65
+ (await publicClient.call({ to, data, blockNumber })).data;
66
+
67
+ const logsAt = (publicClient: PublicClient, address: Address, block: bigint) =>
68
+ publicClient.getLogs({ address, fromBlock: block, toBlock: block });
69
+
70
+ /**
71
+ * Checks one label against the chain: does the ctHash really come from where the label says?
72
+ * `issuer` is the share issuer (`acp.issuer`), which an issuer argument of a stored value stands
73
+ * for. One request.
74
+ */
75
+ export const verifyShareLabel = async (
76
+ label: ShareLabel,
77
+ handle: Hex,
78
+ publicClient: PublicClient,
79
+ issuer: Address
80
+ ): Promise<LabelVerification> => {
81
+ try {
82
+ switch (label.kind) {
83
+ case 'stored':
84
+ return checkStored(
85
+ label,
86
+ handle,
87
+ await callAt(publicClient, label.contract, storedCallData(label, issuer), label.block)
88
+ );
89
+ case 'event':
90
+ return checkEvent(label, handle, await logsAt(publicClient, label.contract, label.block));
91
+ case 'unlabelled':
92
+ return 'unverifiable';
93
+ }
94
+ } catch {
95
+ return 'unverifiable';
96
+ }
97
+ };
98
+
99
+ /**
100
+ * Checks the labels of a share, index for index with `handles`, as far as `verify` says. Requests
101
+ * are shared: one `getLogs` per contract and block, however many values it carries, and one call
102
+ * per distinct stored read.
103
+ */
104
+ export const verifyShareLabels = async ({
105
+ labels,
106
+ handles,
107
+ issuer,
108
+ publicClient,
109
+ verify = 'all',
110
+ }: {
111
+ labels: readonly ShareLabel[];
112
+ handles: readonly Hex[];
113
+ issuer: Address;
114
+ publicClient: PublicClient;
115
+ verify?: ShareLabelVerifyMode;
116
+ }): Promise<ShareLabelCheck[]> => {
117
+ if (labels.length !== handles.length) {
118
+ throw new Error(`share metadata: ${labels.length} labels for ${handles.length} ctHashes`);
119
+ }
120
+
121
+ const requests = new Map<string, Promise<unknown>>();
122
+ const once = <T>(key: string, request: () => Promise<T>): Promise<T> => {
123
+ if (!requests.has(key)) requests.set(key, request());
124
+ return requests.get(key) as Promise<T>;
125
+ };
126
+
127
+ return Promise.all(
128
+ labels.map(async (label, i): Promise<ShareLabelCheck> => {
129
+ const handle = handles[i];
130
+ try {
131
+ switch (label.kind) {
132
+ case 'stored': {
133
+ if (verify !== 'all') return 'skipped';
134
+ const data = storedCallData(label, issuer);
135
+ const key = `call:${label.contract.toLowerCase()}:${label.block}:${data}`;
136
+ return checkStored(
137
+ label,
138
+ handle,
139
+ await once(key, () => callAt(publicClient, label.contract, data, label.block))
140
+ );
141
+ }
142
+ case 'event': {
143
+ if (verify === 'none') return 'skipped';
144
+ const key = `logs:${label.contract.toLowerCase()}:${label.block}`;
145
+ return checkEvent(label, handle, await once(key, () => logsAt(publicClient, label.contract, label.block)));
146
+ }
147
+ case 'unlabelled':
148
+ return verify === 'none' ? 'skipped' : 'unverifiable';
149
+ }
150
+ } catch {
151
+ return 'unverifiable';
152
+ }
153
+ })
154
+ );
155
+ };