@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @cofhe/sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- d711ce2: 1.0.0, the first stable release. Breaking changes since 0.8, detailed in their own entries below:
|
|
8
|
+
|
|
9
|
+
- The on-chain share registry has a new layout (`share(acp, metadata)`, header-only `sharesFor` / `getShare`), and shared ACPs no longer carry a `name`.
|
|
10
|
+
- The mocks need Solidity 0.8.25 or later and `@openzeppelin/contracts-upgradeable`.
|
|
11
|
+
- `CofheProvider` takes either `config` or `cofheClient`, not both.
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 6dcfb7d: ACPs sharing SNAPSHOTs (ctHashes rather than live data) can use the new on-chain ACP Sharing Registry `metadata` field to cite the origin of the snapshotted data (contract address + view-function / event), and the recipient can verify it on-chain. [Docs](https://cofhesdk.fhenix.io/sdk/acp#labelling-a-snapshot-share)
|
|
16
|
+
|
|
17
|
+
**@cofhe/sdk**
|
|
18
|
+
|
|
19
|
+
- `createSharing({ labels })`, `shareOnChain(acp, { labels | metadata })`; labels travel in `export()` / `importShared` as `SharedACP.metadata`.
|
|
20
|
+
- New `client.acp.verifyShareLabels`, `client.acp.getShareFromChain`, and `describeShareMetadata`. `IncomingShare` gains `metadata` and `appKey`.
|
|
21
|
+
- New `acp.app` config option: shares are tagged with the app's key and `getIncomingShares` lists only that app's shares ([docs](https://cofhesdk.fhenix.io/sdk/acp#mark-shares-for-your-app)).
|
|
22
|
+
- `@cofhe/sdk/acps` exports the metadata codec, label builders (`viewFunctionLabel`, `eventLogLabel`, `UNLABELLED`), verifiers, registry readers, `shareAppKey` and `ERC7984_ABI`.
|
|
23
|
+
|
|
24
|
+
**@cofhe/react**
|
|
25
|
+
|
|
26
|
+
- New `useCofheShareLabels`. `useCofheShareOnChain` accepts `{ acp, labels | metadata }`.
|
|
27
|
+
- The inbox shows each share's labels and their verification. [Docs](https://cofhesdk.fhenix.io/react/acp#share-labels)
|
|
28
|
+
|
|
29
|
+
**Mocks and plugins**
|
|
30
|
+
|
|
31
|
+
- `ACPShareRegistry`, `ACPTimestampRevoker` and `Permissioned` are copied verbatim from a pinned cofhe-contracts commit (`pnpm update:cofhe-contracts <commit>`). The share registry is deployed behind `MockERC1967Proxy`.
|
|
32
|
+
- Mocks now need Solidity ≥0.8.25 and `@openzeppelin/contracts-upgradeable` (new Hardhat plugin peer dependency; Foundry users add its remapping).
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- d711ce2: The mocks now behave like the real TaskManager and TFHE-rs in more places. Tests that relied on the old mock behaviour may start failing, as they would on a real network.
|
|
37
|
+
|
|
38
|
+
- `FHE.asE*` (trivial encrypt) reverts with `InvalidInputForFunction` when the value does not fit the type (e.g. `FHE.asEuint8(256)`), and with `InvalidSecurityZone` for an out of range zone, instead of storing the value masked.
|
|
39
|
+
- `encryptInputs` against the mocks rejects out-of-range values with the same error as the production path, instead of passing them on to be masked (e.g. `Encryptable.uint8(256n)` became `0`).
|
|
40
|
+
- `FHE.not` on encrypted integers is a bitwise NOT (`not(euint8(2))` is 253, not 0), and `FHE.rol` / `FHE.ror` rotate within the type's width, with the amount taken modulo the width, instead of shifting.
|
|
41
|
+
- `FHE.rem(x, 0)` returns `x` instead of reverting.
|
|
42
|
+
- `FHE.random*` handles carry their type and security zone and can be allowed and computed on.
|
|
43
|
+
- `MockTaskManager.isPubliclyAllowed` returns whether a handle was made public with `FHE.allowGlobal` / `FHE.allowPublic`, instead of reverting with `NotImplemented`.
|
|
44
|
+
- Handles in a negative security zone go through the ACL like any other handle. Previously any contract could use them and `FHE.allow*` did nothing.
|
|
45
|
+
- `publishDecryptResultBatch` emits `DecryptionResult` with the caller as `requestor`, and the batch decrypt result functions revert with `LengthMismatch` when the arrays differ in length.
|
|
46
|
+
- `setSecurityZones` rejects an inverted range (`min > max`) with `InvalidSecurityZone`.
|
|
47
|
+
|
|
48
|
+
- 5a192ee: Node storage now creates ~/.cofhesdk with mode 0700 and files with 0600, and tightens permissions on existing ones.
|
|
49
|
+
- 757e448: Use the latest chain block timestamp for injected ACP revoker data while preserving explicit revoker configuration precedence.
|
|
50
|
+
- 73d5297: `seal` and `unsealWithPrivateKey` now throw when the sealing key contains non-hex characters. Previously a 64-character key with invalid characters passed the length check and those characters decoded to zero bytes, so `seal` silently encrypted to the wrong key (an entirely invalid key became the all-zero key, which offers no confidentiality), and `unsealWithPrivateKey` failed later with `Failed to decrypt message`.
|
|
51
|
+
- 8fad647: `unsealWithPrivateKey` throws a `CofheError` with code `SealOutputFailed` when a decrypt response can't be unsealed, instead of a plain `Error`. `useCofheDecrypt` no longer retries these failures.
|
|
52
|
+
- a4fcea7: `verifyDecryptResult` now returns `false` when the chain's TaskManager has no `decryptResultSigner` set (zero address). It used to return `true` for any signature in that case. The on-chain `verifyDecryptResult` still skips the check when no signer is set.
|
|
53
|
+
|
|
3
54
|
## 0.8.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
package/acps/acp.ts
CHANGED
|
@@ -27,6 +27,11 @@ import {
|
|
|
27
27
|
import { SignatureUtils } from './signature.js';
|
|
28
28
|
import { GenerateSealingKey, unsealWithPrivateKey } from './sealing.js';
|
|
29
29
|
import { checkACPValidityOnChain, getACPAccessStatusOnChain, getAclEIP712Domain } from './onchain-utils.js';
|
|
30
|
+
import { encodeShareMetadata } from './share-metadata/encode.js';
|
|
31
|
+
import { UNLABELLED } from './share-metadata/presets';
|
|
32
|
+
|
|
33
|
+
/** The name of an imported acp unless the recipient gives one: who it came from. */
|
|
34
|
+
const sharedByName = (issuer: Hex): string => `Shared by ${issuer.slice(0, 6)}…${issuer.slice(-4)}`;
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
37
|
* Main ACP utilities - functional approach for React compatibility
|
|
@@ -57,6 +62,15 @@ export const ACPUtils = {
|
|
|
57
62
|
*/
|
|
58
63
|
createSharing: (options: CreateSharingACPOptions): SharingACP => {
|
|
59
64
|
const validation = validateSharingACPOptions(options);
|
|
65
|
+
const { labels, appKey } = options;
|
|
66
|
+
if (labels != null && labels.length !== validation.handles.length) {
|
|
67
|
+
throw new Error(`Cannot label a share of ${validation.handles.length} handles with ${labels.length} labels`);
|
|
68
|
+
}
|
|
69
|
+
// With an app key but no labels, the handles are listed as unlabelled so the key has a blob to sit in.
|
|
70
|
+
const metadata =
|
|
71
|
+
labels != null || appKey != null
|
|
72
|
+
? encodeShareMetadata(labels ?? validation.handles.map(() => UNLABELLED), { issuer: validation.issuer, appKey })
|
|
73
|
+
: undefined;
|
|
60
74
|
|
|
61
75
|
// Always generate a new sealing key - users cannot provide their own
|
|
62
76
|
const sealingPair = GenerateSealingKey();
|
|
@@ -64,6 +78,7 @@ export const ACPUtils = {
|
|
|
64
78
|
const acp = {
|
|
65
79
|
hash: ACPUtils.getHash(validation),
|
|
66
80
|
...validation,
|
|
81
|
+
...(metadata != null && { metadata }),
|
|
67
82
|
sealingPrivateKey: sealingPair.privateKey,
|
|
68
83
|
sealingKey: sealingPair.publicKey,
|
|
69
84
|
_signedDomain: undefined,
|
|
@@ -73,9 +88,10 @@ export const ACPUtils = {
|
|
|
73
88
|
},
|
|
74
89
|
|
|
75
90
|
/**
|
|
76
|
-
* Import a shared acp from various input formats
|
|
91
|
+
* Import a shared acp from various input formats. The acp is named `name`, or after its issuer
|
|
92
|
+
* ("Shared by 0x1234…abcd"); a `name` in the share itself is ignored.
|
|
77
93
|
*/
|
|
78
|
-
importShared: (options: ImportSharedACPOptions | string): RecipientACP => {
|
|
94
|
+
importShared: (options: ImportSharedACPOptions | string, { name }: { name?: string } = {}): RecipientACP => {
|
|
79
95
|
let parsedOptions: ImportSharedACPOptions;
|
|
80
96
|
|
|
81
97
|
// Handle different input types
|
|
@@ -106,6 +122,7 @@ export const ACPUtils = {
|
|
|
106
122
|
const acp = {
|
|
107
123
|
hash: ACPUtils.getHash(validation),
|
|
108
124
|
...validation,
|
|
125
|
+
name: name?.trim() || sharedByName(validation.issuer),
|
|
109
126
|
sealingPrivateKey: sealingPair.privateKey,
|
|
110
127
|
sealingKey: sealingPair.publicKey,
|
|
111
128
|
_signedDomain: undefined,
|
|
@@ -184,9 +201,10 @@ export const ACPUtils = {
|
|
|
184
201
|
importSharedAndSign: async (
|
|
185
202
|
options: ImportSharedACPOptions | string,
|
|
186
203
|
publicClient: PublicClient,
|
|
187
|
-
walletClient: WalletClient
|
|
204
|
+
walletClient: WalletClient,
|
|
205
|
+
importOptions: { name?: string } = {}
|
|
188
206
|
): Promise<RecipientACP> => {
|
|
189
|
-
const acp = ACPUtils.importShared(options);
|
|
207
|
+
const acp = ACPUtils.importShared(options, importOptions);
|
|
190
208
|
return ACPUtils.sign(acp, publicClient, walletClient);
|
|
191
209
|
},
|
|
192
210
|
|
|
@@ -218,6 +236,7 @@ export const ACPUtils = {
|
|
|
218
236
|
recipientSignature: acp.recipientSignature,
|
|
219
237
|
_signedDomain: acp._signedDomain,
|
|
220
238
|
sealingPrivateKey: acp.sealingPrivateKey,
|
|
239
|
+
...(acp.metadata != null && { metadata: acp.metadata }),
|
|
221
240
|
};
|
|
222
241
|
},
|
|
223
242
|
|
|
@@ -297,7 +316,7 @@ export const ACPUtils = {
|
|
|
297
316
|
/**
|
|
298
317
|
* Export acp data for sharing (strips the private component).
|
|
299
318
|
* Fixed `SharedACP` shape — every field always present, aligned with
|
|
300
|
-
* `ACPPublic` and the on-chain sharing payload.
|
|
319
|
+
* `ACPPublic` and the on-chain sharing payload — plus `metadata` when the acp has labels.
|
|
301
320
|
*/
|
|
302
321
|
export: (acp: ACP): string => {
|
|
303
322
|
if (acp.type !== 'sharing') {
|
|
@@ -312,7 +331,6 @@ export const ACPUtils = {
|
|
|
312
331
|
}
|
|
313
332
|
|
|
314
333
|
const shared: SharedACP = {
|
|
315
|
-
name: acp.name,
|
|
316
334
|
type: acp.type,
|
|
317
335
|
issuer: acp.issuer,
|
|
318
336
|
expiration: acp.expiration,
|
|
@@ -323,6 +341,7 @@ export const ACPUtils = {
|
|
|
323
341
|
contracts: acp.contracts,
|
|
324
342
|
handles: acp.handles,
|
|
325
343
|
issuerSignature: acp.issuerSignature,
|
|
344
|
+
...(acp.metadata != null && acp.metadata !== '0x' && { metadata: acp.metadata }),
|
|
326
345
|
};
|
|
327
346
|
|
|
328
347
|
return JSON.stringify(shared, undefined, 2);
|
package/acps/index.ts
CHANGED
|
@@ -9,18 +9,23 @@ export type {
|
|
|
9
9
|
ACPPublic,
|
|
10
10
|
SharedACP,
|
|
11
11
|
IncomingShare,
|
|
12
|
+
LabelledShare,
|
|
12
13
|
EIP712Domain,
|
|
13
14
|
EIP712Types,
|
|
14
15
|
EIP712Message,
|
|
15
16
|
ValidationResult,
|
|
16
17
|
ACPSignaturePrimaryType,
|
|
17
|
-
} from './types
|
|
18
|
+
} from './types';
|
|
18
19
|
|
|
19
20
|
// Main utilities
|
|
20
|
-
export { ACPUtils } from './acp
|
|
21
|
+
export { ACPUtils } from './acp';
|
|
21
22
|
|
|
22
|
-
// On-chain sharing: registry / revoker ABIs, the registry payload and share id of a sharing ACP
|
|
23
|
-
|
|
23
|
+
// On-chain sharing: registry / revoker ABIs, the registry payload and share id of a sharing ACP,
|
|
24
|
+
// and the read of posted shares from their Shared events
|
|
25
|
+
export * from './registry';
|
|
26
|
+
|
|
27
|
+
// Share metadata: labels saying where each handle of a SNAPSHOT share came from
|
|
28
|
+
export * from './share-metadata/index';
|
|
24
29
|
|
|
25
30
|
// Addresses the chain ACL serves (TaskManager -> acl() -> default revoker, share registry)
|
|
26
31
|
export {
|
|
@@ -28,7 +33,7 @@ export {
|
|
|
28
33
|
getAclServedAddresses,
|
|
29
34
|
clearAclServedAddresses,
|
|
30
35
|
type AclServedAddresses,
|
|
31
|
-
} from './onchain-utils
|
|
36
|
+
} from './onchain-utils';
|
|
32
37
|
|
|
33
38
|
// Validation utilities
|
|
34
39
|
export {
|
|
@@ -49,10 +54,10 @@ export {
|
|
|
49
54
|
validateImportACP,
|
|
50
55
|
// Common utilities
|
|
51
56
|
ValidationUtils,
|
|
52
|
-
} from './validation
|
|
57
|
+
} from './validation';
|
|
53
58
|
|
|
54
59
|
// Signature utilities
|
|
55
|
-
export { SignatureUtils, getSignatureTypesAndMessage, SignatureTypes } from './signature
|
|
60
|
+
export { SignatureUtils, getSignatureTypesAndMessage, SignatureTypes } from './signature';
|
|
56
61
|
|
|
57
62
|
// Storage utilities
|
|
58
63
|
export {
|
|
@@ -67,15 +72,15 @@ export {
|
|
|
67
72
|
removeActiveACPHash,
|
|
68
73
|
clearStaleStore,
|
|
69
74
|
ACP_STORE_DEFAULTS,
|
|
70
|
-
} from './store
|
|
75
|
+
} from './store';
|
|
71
76
|
|
|
72
77
|
// Sealing utilities
|
|
73
|
-
export { GenerateSealingKey, seal, unsealWithPrivateKey, type SealingKeyPair } from './sealing
|
|
74
|
-
export type { EthEncryptedData } from './sealing
|
|
78
|
+
export { GenerateSealingKey, seal, unsealWithPrivateKey, type SealingKeyPair } from './sealing';
|
|
79
|
+
export type { EthEncryptedData } from './sealing';
|
|
75
80
|
|
|
76
81
|
// Re-export everything for convenience
|
|
77
|
-
export * from './types
|
|
78
|
-
export * from './acp
|
|
79
|
-
export * from './validation
|
|
80
|
-
export * from './signature
|
|
81
|
-
export * from './store
|
|
82
|
+
export * from './types';
|
|
83
|
+
export * from './acp';
|
|
84
|
+
export * from './validation';
|
|
85
|
+
export * from './signature';
|
|
86
|
+
export * from './store';
|
package/acps/onchain-utils.ts
CHANGED
|
@@ -345,9 +345,9 @@ export const clearAclServedAddresses = () => aclServedAddressesCache.clear();
|
|
|
345
345
|
* TaskManager -> acl() -> getters). Zero addresses and pre-upgrade ACLs (getters absent -> revert)
|
|
346
346
|
* resolve to `undefined` — callers fall back to `acp.*` config.
|
|
347
347
|
*
|
|
348
|
-
* Resolutions are cached per chainId. A failure to reach the TaskManager
|
|
349
|
-
* error, no CoFHE deployment) is NOT cached, so a transient outage
|
|
350
|
-
* an empty result for the whole session.
|
|
348
|
+
* Resolutions are cached per chainId. A failure to reach the TaskManager or the ACL
|
|
349
|
+
* getters (network error, no CoFHE deployment) is NOT cached, so a transient outage
|
|
350
|
+
* does not pin an empty result for the whole session.
|
|
351
351
|
*/
|
|
352
352
|
export const getAclServedAddresses = async (
|
|
353
353
|
publicClient: PublicClient,
|
|
@@ -367,19 +367,24 @@ export const getAclServedAddresses = async (
|
|
|
367
367
|
return {};
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
// A revert means the getter is missing (pre-upgrade ACL) and is safe to cache. Any other
|
|
371
|
+
// error (rate limit, timeout) is transient: return what we have but don't cache it.
|
|
372
|
+
let transient = false;
|
|
373
|
+
const readServedAddress = (functionName: 'defaultRevokerContract' | 'shareRegistry') =>
|
|
374
|
+
publicClient.readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName }).catch((err) => {
|
|
375
|
+
if (revertErrorName(err, ACL_SERVED_ADDRESSES_ABI) === undefined) transient = true;
|
|
376
|
+
return undefined;
|
|
377
|
+
});
|
|
378
|
+
|
|
370
379
|
const [defaultRevoker, shareRegistry] = await Promise.all([
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
.catch(() => undefined),
|
|
374
|
-
publicClient
|
|
375
|
-
.readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName: 'shareRegistry' })
|
|
376
|
-
.catch(() => undefined),
|
|
380
|
+
readServedAddress('defaultRevokerContract'),
|
|
381
|
+
readServedAddress('shareRegistry'),
|
|
377
382
|
]);
|
|
378
383
|
|
|
379
384
|
const resolved: AclServedAddresses = {
|
|
380
385
|
defaultRevoker: defaultRevoker != null && defaultRevoker !== zeroAddress ? defaultRevoker : undefined,
|
|
381
386
|
shareRegistry: shareRegistry != null && shareRegistry !== zeroAddress ? shareRegistry : undefined,
|
|
382
387
|
};
|
|
383
|
-
aclServedAddressesCache.set(chainId, resolved);
|
|
388
|
+
if (!transient) aclServedAddressesCache.set(chainId, resolved);
|
|
384
389
|
return resolved;
|
|
385
390
|
};
|
package/acps/registry.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
encodeAbiParameters,
|
|
3
|
+
keccak256,
|
|
4
|
+
parseAbi,
|
|
5
|
+
type AbiParameterToPrimitiveType,
|
|
6
|
+
type Address,
|
|
7
|
+
type Hex,
|
|
8
|
+
type PublicClient,
|
|
9
|
+
} from 'viem';
|
|
2
10
|
import { ACPUtils } from './acp.js';
|
|
3
11
|
import type { ACP } from './types.js';
|
|
4
12
|
|
|
5
13
|
/**
|
|
6
14
|
* The on-chain sharing pieces: the share registry and revoker ABIs, the registry payload of a
|
|
7
|
-
* sharing ACP and its share id
|
|
8
|
-
* transaction path and still agree with the registry
|
|
15
|
+
* sharing ACP and its share id, and the read of a share from its `Shared` event. Exported so an
|
|
16
|
+
* app can send these writes through its own transaction path and still agree with the registry
|
|
17
|
+
* on ids.
|
|
18
|
+
*
|
|
19
|
+
* The registry is pointer-based: storage keeps a share header (issuer, expiration, recipient,
|
|
20
|
+
* revoker, and the block of its `Shared` event); the full payload and the metadata blob travel in
|
|
21
|
+
* that event. A reader takes headers from `sharesFor` / `getShare` and fetches the events with one
|
|
22
|
+
* single-block `getLogs` per block.
|
|
9
23
|
*/
|
|
10
24
|
|
|
11
25
|
// ACP default revoker (timestamp-based revocation) — interface shared by all revokers
|
|
@@ -19,11 +33,14 @@ export const ACP_REVOKER_ABI = parseAbi([
|
|
|
19
33
|
|
|
20
34
|
export const ACP_SHARE_REGISTRY_ABI = parseAbi([
|
|
21
35
|
'struct ACP { address issuer; uint64 expiration; address recipient; uint256 revokerData; address revokerContract; uint8 scope; address[] contracts; bytes32[] handles; bytes32 sealingKey; bytes issuerSignature; bytes recipientSignature; }',
|
|
22
|
-
'
|
|
36
|
+
'struct ShareHeader { address issuer; uint64 expiration; address recipient; uint64 blockNumber; address revokerContract; uint256 revokerData; }',
|
|
37
|
+
'function share(ACP calldata acp, bytes calldata metadata) external returns (bytes32)',
|
|
23
38
|
'function removeShare(bytes32 shareId) external',
|
|
24
|
-
'function sharesFor(address recipient) external view returns (
|
|
25
|
-
'function getShare(bytes32 shareId) external view returns (
|
|
39
|
+
'function sharesFor(address recipient) external view returns (bytes32[] shareIds, ShareHeader[] headers)',
|
|
40
|
+
'function getShare(bytes32 shareId) external view returns (ShareHeader memory)',
|
|
26
41
|
'function isShareValid(bytes32 shareId) external view returns (bool)',
|
|
42
|
+
'event Shared(address indexed recipient, address indexed issuer, bytes32 indexed shareId, ACP acp, bytes metadata)',
|
|
43
|
+
'event ShareRemoved(address indexed recipient, address indexed issuer, bytes32 indexed shareId)',
|
|
27
44
|
]);
|
|
28
45
|
|
|
29
46
|
export const ACP_SHARE_TUPLE_ABI = [
|
|
@@ -68,6 +85,147 @@ export const computeShareId = (acp: ACP): Hex => {
|
|
|
68
85
|
return keccak256(encodeAbiParameters(ACP_SHARE_TUPLE_ABI, [p]));
|
|
69
86
|
};
|
|
70
87
|
|
|
71
|
-
/** The share id of a registry payload as read back from `
|
|
72
|
-
export const shareIdOfChainShare = (share:
|
|
88
|
+
/** The share id of a registry payload as read back from a `Shared` event. */
|
|
89
|
+
export const shareIdOfChainShare = (share: ChainShare): Hex =>
|
|
73
90
|
keccak256(encodeAbiParameters(ACP_SHARE_TUPLE_ABI, [share]));
|
|
91
|
+
|
|
92
|
+
/** A registry payload, as decoded from a `Shared` event (or passed to `share`). */
|
|
93
|
+
export type ChainShare = AbiParameterToPrimitiveType<(typeof ACP_SHARE_TUPLE_ABI)[0]>;
|
|
94
|
+
|
|
95
|
+
/** What the registry stores of a share: the fields it checks, and the block of its `Shared` event. */
|
|
96
|
+
export type ShareHeader = {
|
|
97
|
+
issuer: Address;
|
|
98
|
+
expiration: bigint;
|
|
99
|
+
recipient: Address;
|
|
100
|
+
/** Block of the `Shared` event (the L2 block on Arbitrum). */
|
|
101
|
+
blockNumber: bigint;
|
|
102
|
+
revokerContract: Address;
|
|
103
|
+
revokerData: bigint;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** A share as posted: its payload and the metadata blob that came with it (`0x` when none). */
|
|
107
|
+
export type PostedShare = { shareId: Hex; header: ShareHeader; share: ChainShare; metadata: Hex };
|
|
108
|
+
|
|
109
|
+
/** A share the registry lists but whose `Shared` event could not be read, and why. */
|
|
110
|
+
export type UnreadableShare = { shareId: Hex; header: ShareHeader; reason: string };
|
|
111
|
+
|
|
112
|
+
/** Shares read from their events, and those that could not be; each share is on its own. */
|
|
113
|
+
export type PostedShares = { posted: PostedShare[]; unreadable: UnreadableShare[] };
|
|
114
|
+
|
|
115
|
+
const messageOf = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fetches the `Shared` events of shares from the blocks their headers name: one single-block
|
|
119
|
+
* `getLogs` per distinct block, filtered by registry and share ids, so any node answers it. Returns
|
|
120
|
+
* the shares in the order given; where a block holds several events for one share id, the last.
|
|
121
|
+
* Never fails as a whole: a share whose block the node cannot serve yet, whose event is missing
|
|
122
|
+
* from it, or whose event payload does not hash to its id is returned as `unreadable`, with the
|
|
123
|
+
* reason, and the rest are posted as usual.
|
|
124
|
+
*/
|
|
125
|
+
export const readPostedShares = async (
|
|
126
|
+
publicClient: PublicClient,
|
|
127
|
+
registry: Address,
|
|
128
|
+
headers: readonly { shareId: Hex; header: ShareHeader }[]
|
|
129
|
+
): Promise<PostedShares> => {
|
|
130
|
+
const idsByBlock = new Map<bigint, Hex[]>();
|
|
131
|
+
for (const { shareId, header } of headers) {
|
|
132
|
+
idsByBlock.set(header.blockNumber, [...(idsByBlock.get(header.blockNumber) ?? []), shareId]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type Found = { share: ChainShare; metadata: Hex } | { reason: string };
|
|
136
|
+
const found = new Map<string, Found>();
|
|
137
|
+
await Promise.all(
|
|
138
|
+
[...idsByBlock].map(async ([block, shareIds]) => {
|
|
139
|
+
let logs;
|
|
140
|
+
try {
|
|
141
|
+
logs = await publicClient.getContractEvents({
|
|
142
|
+
address: registry,
|
|
143
|
+
abi: ACP_SHARE_REGISTRY_ABI,
|
|
144
|
+
eventName: 'Shared',
|
|
145
|
+
args: { shareId: shareIds },
|
|
146
|
+
fromBlock: block,
|
|
147
|
+
toBlock: block,
|
|
148
|
+
strict: true,
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
for (const shareId of shareIds) {
|
|
152
|
+
found.set(shareId.toLowerCase(), {
|
|
153
|
+
reason: `the Shared events of block ${block} could not be read: ${messageOf(error)}`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Logs come in log-index order. A share removed and posted again within one block has two
|
|
159
|
+
// Shared events there; the later one is the share, so each log overwrites the previous.
|
|
160
|
+
for (const { args } of logs) {
|
|
161
|
+
const hashes = shareIdOfChainShare(args.acp).toLowerCase() === args.shareId.toLowerCase();
|
|
162
|
+
found.set(
|
|
163
|
+
args.shareId.toLowerCase(),
|
|
164
|
+
hashes
|
|
165
|
+
? { share: args.acp, metadata: args.metadata }
|
|
166
|
+
: { reason: 'the payload in its Shared event does not hash to its id' }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const result: PostedShares = { posted: [], unreadable: [] };
|
|
173
|
+
for (const { shareId, header } of headers) {
|
|
174
|
+
const entry = found.get(shareId.toLowerCase());
|
|
175
|
+
if (entry == null) {
|
|
176
|
+
result.unreadable.push({
|
|
177
|
+
shareId,
|
|
178
|
+
header,
|
|
179
|
+
reason: `no Shared event in block ${header.blockNumber}, where the registry points`,
|
|
180
|
+
});
|
|
181
|
+
} else if ('reason' in entry) {
|
|
182
|
+
result.unreadable.push({ shareId, header, reason: entry.reason });
|
|
183
|
+
} else {
|
|
184
|
+
result.posted.push({ shareId, header, ...entry });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The importable shares addressed to `recipient` (unexpired, not revoked), read from their events;
|
|
192
|
+
* those whose event could not be read are listed apart, so one of them never hides the rest.
|
|
193
|
+
*/
|
|
194
|
+
export const readSharesFor = async (
|
|
195
|
+
publicClient: PublicClient,
|
|
196
|
+
registry: Address,
|
|
197
|
+
recipient: Address
|
|
198
|
+
): Promise<PostedShares> => {
|
|
199
|
+
const [shareIds, headers] = await publicClient.readContract({
|
|
200
|
+
address: registry,
|
|
201
|
+
abi: ACP_SHARE_REGISTRY_ABI,
|
|
202
|
+
functionName: 'sharesFor',
|
|
203
|
+
args: [recipient],
|
|
204
|
+
});
|
|
205
|
+
return readPostedShares(
|
|
206
|
+
publicClient,
|
|
207
|
+
registry,
|
|
208
|
+
shareIds.map((shareId, i) => ({ shareId, header: headers[i] }))
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* One share by id, read from its event; null when the registry has no such share (unknown or
|
|
214
|
+
* removed). Throws, with the reason, when the registry lists it but its event could not be read.
|
|
215
|
+
*/
|
|
216
|
+
export const readShare = async (
|
|
217
|
+
publicClient: PublicClient,
|
|
218
|
+
registry: Address,
|
|
219
|
+
shareId: Hex
|
|
220
|
+
): Promise<PostedShare | null> => {
|
|
221
|
+
const header = await publicClient.readContract({
|
|
222
|
+
address: registry,
|
|
223
|
+
abi: ACP_SHARE_REGISTRY_ABI,
|
|
224
|
+
functionName: 'getShare',
|
|
225
|
+
args: [shareId],
|
|
226
|
+
});
|
|
227
|
+
if (header.issuer === '0x0000000000000000000000000000000000000000') return null;
|
|
228
|
+
const { posted, unreadable } = await readPostedShares(publicClient, registry, [{ shareId, header }]);
|
|
229
|
+
if (unreadable.length > 0) throw new Error(`Share ${shareId}: ${unreadable[0].reason}`);
|
|
230
|
+
return posted[0];
|
|
231
|
+
};
|
package/acps/sealing.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import nacl from 'tweetnacl';
|
|
2
2
|
import { type Hex } from 'viem';
|
|
3
3
|
import { fromHexString, toBeArray, toBigInt, toHexString, isBigIntOrNumber, isString } from './utils.js';
|
|
4
|
+
import { CofheError, CofheErrorCode } from '../core/error.js';
|
|
4
5
|
|
|
5
6
|
/** 32-byte X25519 keys as bare hex (no 0x prefix) */
|
|
6
7
|
const KEY_HEX_LENGTH = 64;
|
|
@@ -46,7 +47,7 @@ export const GenerateSealingKey = (): SealingKeyPair => {
|
|
|
46
47
|
export const seal = (value: bigint | number, publicKey: string): EthEncryptedData => {
|
|
47
48
|
isString(publicKey);
|
|
48
49
|
isBigIntOrNumber(value);
|
|
49
|
-
|
|
50
|
+
assertSealingKey(publicKey, 'Public');
|
|
50
51
|
|
|
51
52
|
// generate ephemeral keypair
|
|
52
53
|
const ephemeralKeyPair = nacl.box.keyPair();
|
|
@@ -67,24 +68,49 @@ export const seal = (value: bigint | number, publicKey: string): EthEncryptedDat
|
|
|
67
68
|
* The ephemeral public key travels inside the payload, so the private key alone suffices.
|
|
68
69
|
*/
|
|
69
70
|
export const unsealWithPrivateKey = (privateKey: string, parsedData: EthEncryptedData): bigint => {
|
|
70
|
-
|
|
71
|
+
assertSealingKey(privateKey, 'Private');
|
|
71
72
|
|
|
72
73
|
const nonce = parsedData.nonce instanceof Uint8Array ? parsedData.nonce : new Uint8Array(parsedData.nonce);
|
|
73
74
|
const ephemPublicKey =
|
|
74
75
|
parsedData.public_key instanceof Uint8Array ? parsedData.public_key : new Uint8Array(parsedData.public_key);
|
|
75
76
|
const dataToDecrypt = parsedData.data instanceof Uint8Array ? parsedData.data : new Uint8Array(parsedData.data);
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
// nacl returns null for a wrong key or tampered payload and throws on malformed inputs. Either way it
|
|
79
|
+
// is a CofheError, so callers that stop retrying on those (e.g. `useCofheDecrypt`) do not re-request.
|
|
80
|
+
let decryptedMessage: Uint8Array | null;
|
|
81
|
+
let cause: Error | undefined;
|
|
82
|
+
try {
|
|
83
|
+
decryptedMessage = nacl.box.open(dataToDecrypt, nonce, ephemPublicKey, fromHexString(privateKey));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
decryptedMessage = null;
|
|
86
|
+
cause = e instanceof Error ? e : undefined;
|
|
87
|
+
}
|
|
78
88
|
if (!decryptedMessage) {
|
|
79
|
-
|
|
89
|
+
// Lengths only: the context must not carry the private key or any sealing material.
|
|
90
|
+
throw new CofheError({
|
|
91
|
+
code: CofheErrorCode.SealOutputFailed,
|
|
92
|
+
message: 'Failed to unseal decrypt response',
|
|
93
|
+
cause,
|
|
94
|
+
context: {
|
|
95
|
+
dataLength: dataToDecrypt.length,
|
|
96
|
+
nonceLength: nonce.length,
|
|
97
|
+
publicKeyLength: ephemPublicKey.length,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
80
100
|
}
|
|
81
101
|
|
|
82
102
|
return toBigInt(decryptedMessage);
|
|
83
103
|
};
|
|
84
104
|
|
|
85
|
-
const
|
|
105
|
+
const assertSealingKey = (key: string, kind: 'Private' | 'Public'): void => {
|
|
86
106
|
const bare = key.startsWith('0x') ? key.slice(2) : key;
|
|
87
107
|
if (bare.length !== KEY_HEX_LENGTH) {
|
|
88
108
|
throw new Error(`${kind} key must be of length ${KEY_HEX_LENGTH}`);
|
|
89
109
|
}
|
|
110
|
+
// A correct-length key with non-hex characters would otherwise decode those characters to
|
|
111
|
+
// zero bytes. An entirely invalid public key becomes the all-zero key, a low-order point, so
|
|
112
|
+
// anything sealed to it could be opened without the matching private key.
|
|
113
|
+
if (!/^[0-9a-fA-F]+$/.test(bare)) {
|
|
114
|
+
throw new Error(`${kind} key must contain only hex characters`);
|
|
115
|
+
}
|
|
90
116
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { keccak256, stringToHex, type Hex } from 'viem';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The key an app marks its shares with: keccak256 of its name, e.g. `shareAppKey('explorer.fhenix.io')`.
|
|
5
|
+
* A client configured with `acp: { app }` puts it on every share it creates and lists only the
|
|
6
|
+
* shares that carry it; the name itself is never on-chain.
|
|
7
|
+
*/
|
|
8
|
+
export const shareAppKey = (app: string): Hex => keccak256(stringToHex(app));
|