@cofhe/sdk 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/acps/acp.ts +10 -1
- package/acps/index.ts +11 -0
- package/acps/onchain-utils.ts +188 -49
- package/acps/registry.ts +73 -0
- package/acps/test/onchainAccessStatus.test.ts +116 -0
- package/acps/types.ts +16 -0
- package/chains/chains/arbitrum.ts +14 -0
- package/chains/chains/ethereum.ts +14 -0
- package/chains/index.ts +5 -1
- package/chains/test/chains.test.ts +24 -3
- package/core/acps.ts +51 -143
- package/core/client.ts +13 -8
- package/core/clientTypes.ts +29 -4
- package/core/config.ts +25 -1
- package/core/consts.ts +14 -2
- package/core/decrypt/verifyDecryptResult.ts +4 -3
- package/core/encrypt/encryptInputsBuilder.ts +6 -0
- package/core/encrypt/zkPackProveVerify.ts +7 -1
- package/core/error.ts +1 -0
- package/core/fetchKeys.ts +2 -2
- package/core/index.ts +9 -1
- package/core/keyStore.ts +23 -10
- package/core/taskManager.ts +48 -0
- package/core/test/acps.test.ts +91 -1
- package/core/test/client.test.ts +41 -0
- package/core/test/config.test.ts +16 -0
- package/core/test/decrypt.test.ts +11 -4
- package/core/test/encryptInputsBuilder.test.ts +1 -1
- package/core/test/fetchKeys.test.ts +23 -2
- package/core/test/keyStore.test.ts +18 -6
- package/core/test/taskManager.test.ts +57 -0
- package/core/types.ts +8 -0
- package/dist/acps.cjs +314 -35
- package/dist/acps.d.cts +251 -4
- package/dist/acps.d.ts +251 -4
- package/dist/acps.js +2 -2
- package/dist/chains.cjs +26 -0
- package/dist/chains.d.cts +45 -1
- package/dist/chains.d.ts +45 -1
- package/dist/chains.js +1 -1
- package/dist/{chunk-43USWPEH.js → chunk-GGI7Y6AW.js} +59 -264
- package/dist/chunk-KSOV5B4B.js +50 -0
- package/dist/{chunk-ESMZCFJY.js → chunk-NMKMCAI3.js} +4 -2
- package/dist/{chunk-Q7CBWGQX.js → chunk-UJYOBHYA.js} +374 -37
- package/dist/{chunk-N6IDQRRU.js → chunk-UKQAJK4N.js} +25 -1
- package/dist/{clientTypes-BN3nbzYM.d.ts → clientTypes-5YPPR16m.d.ts} +77 -14
- package/dist/{clientTypes-CYZjFznO.d.cts → clientTypes-D3aIXmUp.d.cts} +77 -14
- package/dist/core.cjs +271 -136
- package/dist/core.d.cts +29 -8
- package/dist/core.d.ts +29 -8
- package/dist/core.js +4 -4
- package/dist/node.cjs +266 -136
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +5 -5
- package/dist/{acp-Wi6isVQI.d.cts → onchain-utils-CUAK8glx.d.cts} +31 -1
- package/dist/{acp-Wi6isVQI.d.ts → onchain-utils-CUAK8glx.d.ts} +31 -1
- package/dist/web.cjs +360 -150
- package/dist/web.d.cts +49 -3
- package/dist/web.d.ts +49 -3
- package/dist/web.js +51 -20
- package/dist/zkProve.worker.cjs +69 -15
- package/dist/zkProve.worker.js +23 -17
- package/node/storage.ts +2 -2
- package/node/test/inherited.test.ts +5 -1
- package/node/test/storage-memory-fallback.test.ts +47 -0
- package/package.json +5 -3
- package/web/index.ts +100 -26
- package/web/test/countingZkProveWorker.ts +31 -0
- package/web/test/inherited.web.test.ts +5 -1
- package/web/test/mainThreadPool.web.test.ts +174 -0
- package/web/test/threadPoolBenchHelper.ts +140 -0
- package/web/test/threadPoolOff.web.test.ts +17 -0
- package/web/test/threadPoolOn.web.test.ts +80 -0
- package/web/test/workerInitRace.web.test.ts +79 -0
- package/web/tfheThreadPool.ts +140 -0
- package/web/workerManager.ts +4 -2
- package/web/zkProve.worker.ts +42 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @cofhe/sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0fe40c3: Add the `ethereum` (chain id 1) and `arbitrum` (Arbitrum One, chain id 42161) mainnet chains to `@cofhe/sdk/chains`, both with `environment: 'MAINNET'`. They are included in `chains`, `getChainById` and `getChainByName`.
|
|
8
|
+
- 6f7a4c5: Resolve the Task Manager through the `CoFHEAddressBook` (`@fhenixprotocol/cofhe-contracts@1.0.0`). **Breaking:** `TASK_MANAGER_ADDRESS` is removed from `@cofhe/sdk`. Use `getTaskManagerAddress(publicClient)`, which reads `getTm(TASK_MANAGER_ID)` from the book at `COFHE_ADDRESS_BOOK_ADDRESS` (both exported) and caches the result per chain. If the chain has no book, or the id is unset, it throws `TASK_MANAGER_UNRESOLVED`. `verifyDecryptResult`, the ACL/ACP lookups and `useCofheEnabled` / `useCofheReadDecryptionResults` now resolve the Task Manager this way. `@cofhe/react` adds `useCofheTaskManagerAddress()`.
|
|
9
|
+
|
|
10
|
+
Mocks: a new `MockCoFHEAddressBook` is deployed at FHE.sol's `COFHE_ADDRESS_BOOK`, and `MockTaskManager` moves to `MOCKS_TASK_MANAGER_ADDRESS` (`0x…5000`) and is registered in the book. `MockACL` now stores its Task Manager (`setTaskManager` / `getTaskManagerAddress`) instead of reading a constant, so its `TASK_MANAGER_ADDRESS_()` getter is gone. All three plugins deploy and wire the book. Hardhat 2 exposes `hre.cofhe.mocks.getMockCoFHEAddressBook()`, Hardhat 3 exposes `conn.cofhe.mocks.MockCoFHEAddressBook`, and Foundry exposes `CofheTest.mockAddressBook`. Contracts must be built against `@fhenixprotocol/cofhe-contracts@1.0.0` or later (the hardhat plugin's peer range is now `>=1.0.0`). Each FHE op now makes one extra `getTm` call on the book, as it does on real networks, so expect slightly higher gas numbers.
|
|
11
|
+
|
|
12
|
+
- 552d118: The CRS is now cached per security zone. It was fetched per zone but cached under the chain id alone, so after encrypting on zone 0, encrypting on zone 1 reused zone 0's CRS and produced a proof the ZK verifier rejects.
|
|
13
|
+
|
|
14
|
+
**Breaking (`KeysStorage`):** `crs` is now keyed by chain and zone, like `fhe`. `getCrs(chainId, securityZone = 0)` takes the zone, and `setCrs(chainId, securityZone, crs)` matches `setFheKey`'s argument order. A CRS persisted in the old per-chain shape is dropped on rehydrate and refetched.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 17ff23e: Scoped decryption with shared ACPs.
|
|
19
|
+
|
|
20
|
+
**@cofhe/sdk**
|
|
21
|
+
|
|
22
|
+
- `client.acp.importShared` and `client.acp.importFromChain` take an `activate` option (default `true`). With `activate: false` the imported ACP is stored without becoming the active ACP, so decrypts that use the active ACP are unaffected; use the imported ACP explicitly, e.g. `.withACP(acp)`. `importShared`'s second argument is now `{ activate?, publicClient?, walletClient? }`; passing `{ publicClient, walletClient }` works as before.
|
|
23
|
+
- New `client.acp.checkAccess(acp, handle?)` (and `ACPUtils.checkAccessOnChain`) returns an ACP on-chain status instead of reverting: `valid`, `expired`, `revoked` or an invalid signature, and with a handle `allowed`, `out-of-scope` or `issuer-not-allowed`. New exported type: `ACPAccessStatus`. ACL reverts are now recognized when the public client comes from another copy of viem than the SDK, which also fixes `checkValidityOnChain` throwing a generic error in that setup. ACL reverts reported only as text (`custom error 0x…`, e.g. by a transport that drops the error data) are decoded too. For the issuer copy of a share (type `sharing`, no recipient signature yet) the handle-less check reports expiry and revocation instead of failing on the missing signature.
|
|
24
|
+
- `@cofhe/sdk/acps` exports the on-chain sharing pieces: `ACP_SHARE_REGISTRY_ABI`, `ACP_REVOKER_ABI`, `ACP_SHARE_TUPLE_ABI`, `toChainShare`, `computeShareId`, `shareIdOfChainShare`, `getAclAddress`, `getAclServedAddresses` and `clearAclServedAddresses`, so an app can send registry and revoker writes through its own transaction path and still agree with the registry on share ids.
|
|
25
|
+
|
|
26
|
+
**@cofhe/react**
|
|
27
|
+
|
|
28
|
+
- Decrypt with a chosen ACP in part of the tree without changing the active ACP. New `<CofheACPScope acp={acp | hash}>` and `useCofheACPScope()`; `useCofheReadContractAndDecrypt`, `useCofheReadContract`, `useCofheReadContracts` and `useCofheTokenDecryptedBalance` take an `acp` option that wins over the scope. Inside a scope `useCofheTokenDecryptedBalance` reads the ACP issuer balance unless given an account, and an invalid or unknown scoped ACP disables the reads (`disabledDueToMissingValidACP`) instead of falling back to the active ACP. The decrypt cache key gains a fifth segment, the hash of the ACP that decrypts (the chosen one, else the active one), so a value decrypted with one ACP never answers for another. New exported types: `CofheACPInput`, `CofheACPScopeValue`.
|
|
29
|
+
- `<CofheACPScope>` and the per-hook `acp` option check the chosen ACP on chain (`client.acp.checkAccess`) when mounted, every minute and on window focus, so a revoked share turns the scope off and drops its decrypted values while the view is open. `useCofheACPScope()` gains `status` (`checking`, `valid`, `expired`, `revoked`, `invalid`, `unverified`); the scope decrypts only while it is `valid`. For a SNAPSHOT (handle-scope) ACP, `useCofheReadContractAndDecrypt` and `useCofheTokenDecryptedBalance` report `isOutOfScope` for a value the share does not list and never send it for decryption. New exported type: `CofheACPStatus`.
|
|
30
|
+
- Plaintext decrypted with a shared (non-self) ACP is kept in memory only: it is no longer written to the persisted query cache. Decrypts are cached under the ACP that made them and dropped when that ACP leaves the ACP store (e.g. `removeACP`) or when a non-self ACP expires while in use, so a revoked or expired share does not keep serving its values from cache.
|
|
31
|
+
- The sharing hooks are exported: `useCofheIncomingShares` (registry inbox, polled; shares already imported by the connected account are left out), `useCofheShareOnChain`, `useCofheImportShared({ activate })` (exported JSON or an `IncomingShare`), `useCofheRemoveShare`, `useCofheRevokeACP` and `useCofheACPStatus` (an ACP on-chain status, re-read after a revoke mines). The write hooks resolve once mined. The package-internal names (`useIncomingShares`, `useShareOnChain`, `useImportFromChain`, `useRemoveShare`) stay as deprecated aliases.
|
|
32
|
+
- New `useCofheACPs({ chainId?, type? })`: the connected account stored ACPs on a chain (the connected chain by default), optionally narrowed to one type, e.g. `useCofheACPs({ type: 'recipient' })` for received shares. `useCofheAllACPs` is deprecated: it returned only the connected account ACPs on the connected chain, not all ACPs, and is now an alias of `useCofheACPs()`.
|
|
33
|
+
- `useCofheDecrypt` is exported: decrypt a ciphertext handle directly (no contract read), with the same `acp` option and scope behaviour as `useCofheReadContractAndDecrypt`.
|
|
34
|
+
|
|
35
|
+
- 22eebcc: The Node in-memory storage fallback (used when the filesystem key cache is not writable) no longer JSON-encodes values a second time. Previously cached FHE keys and CRS read back from the fallback as strings instead of objects, so they were effectively lost and refetched.
|
|
36
|
+
- 42fccae: Add `tfheThreads` config option (`'auto'` | number | `false`, default `'auto'`) to multi-thread ZK proof generation on cross-origin-isolated web pages, and export the `TfheThreadsSetting` and `TfheInitializer` types.
|
|
37
|
+
|
|
38
|
+
## 0.7.1
|
|
39
|
+
|
|
3
40
|
## 0.7.0
|
|
4
41
|
|
|
5
42
|
### Minor Changes
|
package/acps/acp.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { keccak256, toHex, zeroAddress, parseAbi, type Hex, type PublicClient, type WalletClient } from 'viem';
|
|
2
2
|
import {
|
|
3
|
+
type ACPAccessStatus,
|
|
3
4
|
type ACP,
|
|
4
5
|
type SelfACP,
|
|
5
6
|
type SharingACP,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
} from './validation.js';
|
|
26
27
|
import { SignatureUtils } from './signature.js';
|
|
27
28
|
import { GenerateSealingKey, unsealWithPrivateKey } from './sealing.js';
|
|
28
|
-
import { checkACPValidityOnChain, getAclEIP712Domain } from './onchain-utils.js';
|
|
29
|
+
import { checkACPValidityOnChain, getACPAccessStatusOnChain, getAclEIP712Domain } from './onchain-utils.js';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Main ACP utilities - functional approach for React compatibility
|
|
@@ -408,4 +409,12 @@ export const ACPUtils = {
|
|
|
408
409
|
const publicAcp = ACPUtils.getPublic(acp);
|
|
409
410
|
return checkACPValidityOnChain(publicAcp, publicClient);
|
|
410
411
|
},
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The acp on-chain status as a value instead of a revert: 'valid' or why not, and given a
|
|
415
|
+
* handle, whether the acp may read it (see `ACPAccessStatus`).
|
|
416
|
+
*/
|
|
417
|
+
checkAccessOnChain: async (acp: ACP, publicClient: PublicClient, handle?: bigint | Hex): Promise<ACPAccessStatus> => {
|
|
418
|
+
return getACPAccessStatusOnChain(ACPUtils.getPublic(acp), publicClient, handle);
|
|
419
|
+
},
|
|
411
420
|
};
|
package/acps/index.ts
CHANGED
|
@@ -19,6 +19,17 @@ export type {
|
|
|
19
19
|
// Main utilities
|
|
20
20
|
export { ACPUtils } from './acp.js';
|
|
21
21
|
|
|
22
|
+
// On-chain sharing: registry / revoker ABIs, the registry payload and share id of a sharing ACP
|
|
23
|
+
export * from './registry.js';
|
|
24
|
+
|
|
25
|
+
// Addresses the chain ACL serves (TaskManager -> acl() -> default revoker, share registry)
|
|
26
|
+
export {
|
|
27
|
+
getAclAddress,
|
|
28
|
+
getAclServedAddresses,
|
|
29
|
+
clearAclServedAddresses,
|
|
30
|
+
type AclServedAddresses,
|
|
31
|
+
} from './onchain-utils.js';
|
|
32
|
+
|
|
22
33
|
// Validation utilities
|
|
23
34
|
export {
|
|
24
35
|
// Self ACP validators
|
package/acps/onchain-utils.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type Hex,
|
|
5
|
-
type PublicClient,
|
|
6
|
-
decodeErrorResult,
|
|
7
|
-
parseAbi,
|
|
8
|
-
} from 'viem';
|
|
9
|
-
import type { EIP712Domain, ACPPublic } from './types';
|
|
10
|
-
import { TASK_MANAGER_ADDRESS } from '../core/consts.js';
|
|
1
|
+
import { type Hex, type PublicClient, decodeErrorResult, parseAbi, zeroAddress } from 'viem';
|
|
2
|
+
import type { ACPAccessStatus, EIP712Domain, ACPPublic } from './types';
|
|
3
|
+
import { getTaskManagerAddress } from '../core/taskManager.js';
|
|
11
4
|
|
|
12
5
|
export const getAclAddress = async (publicClient: PublicClient): Promise<Hex> => {
|
|
13
6
|
const ACL_IFACE = 'function acl() view returns (address)';
|
|
@@ -17,7 +10,7 @@ export const getAclAddress = async (publicClient: PublicClient): Promise<Hex> =>
|
|
|
17
10
|
|
|
18
11
|
// Get the ACL address
|
|
19
12
|
return (await publicClient.readContract({
|
|
20
|
-
address:
|
|
13
|
+
address: await getTaskManagerAddress(publicClient),
|
|
21
14
|
abi: aclAbi,
|
|
22
15
|
functionName: 'acl',
|
|
23
16
|
})) as `0x${string}`;
|
|
@@ -59,6 +52,48 @@ export const getAclEIP712Domain = async (publicClient: PublicClient): Promise<EI
|
|
|
59
52
|
};
|
|
60
53
|
};
|
|
61
54
|
|
|
55
|
+
const toACPTuple = (acp: ACPPublic) => ({
|
|
56
|
+
issuer: acp.issuer,
|
|
57
|
+
expiration: BigInt(acp.expiration),
|
|
58
|
+
recipient: acp.recipient,
|
|
59
|
+
revokerData: BigInt(acp.revokerData),
|
|
60
|
+
revokerContract: acp.revokerContract,
|
|
61
|
+
scope: acp.scope,
|
|
62
|
+
contracts: acp.contracts,
|
|
63
|
+
handles: acp.handles,
|
|
64
|
+
sealingKey: acp.sealingKey,
|
|
65
|
+
issuerSignature: acp.issuerSignature,
|
|
66
|
+
recipientSignature: acp.recipientSignature,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The custom error a reverted ACL call carries, however the node reports it: a viem revert, a
|
|
71
|
+
* Hardhat-style 'reverted with custom error' detail, or raw return data. `undefined` when the
|
|
72
|
+
* failure is not a contract revert (e.g. a network error).
|
|
73
|
+
*/
|
|
74
|
+
function revertErrorName(err: unknown, abi: readonly any[]): string | undefined {
|
|
75
|
+
// Viem revert. Matched by name, not `instanceof`: the public client may come from another copy of
|
|
76
|
+
// viem than this package (an app bringing its own), and then no class check ever matches.
|
|
77
|
+
const walk = (err as { walk?: (fn: (e: unknown) => boolean) => unknown } | null)?.walk;
|
|
78
|
+
if (typeof walk === 'function') {
|
|
79
|
+
const revertError = walk.call(err, (e) => (e as { name?: string })?.name === 'ContractFunctionRevertedError') as
|
|
80
|
+
| { data?: { errorName?: string } }
|
|
81
|
+
| null
|
|
82
|
+
| undefined;
|
|
83
|
+
if (revertError) return revertError.data?.errorName ?? '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check details field for custom error names (e.g., from Hardhat test nodes)
|
|
87
|
+
const customErrorName = extractCustomErrorFromDetails(err, abi);
|
|
88
|
+
if (customErrorName) return customErrorName;
|
|
89
|
+
|
|
90
|
+
// Hardhat wrapped error will need to be unwrapped to get the return data
|
|
91
|
+
const hhDetailsData = extractReturnData(err);
|
|
92
|
+
if (hhDetailsData != null) return decodeErrorResult({ abi, data: hhDetailsData }).errorName;
|
|
93
|
+
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
62
97
|
export const checkACPValidityOnChain = async (acp: ACPPublic, publicClient: PublicClient): Promise<boolean> => {
|
|
63
98
|
const aclAddress = await getAclAddress(publicClient);
|
|
64
99
|
|
|
@@ -68,51 +103,69 @@ export const checkACPValidityOnChain = async (acp: ACPPublic, publicClient: Publ
|
|
|
68
103
|
address: aclAddress,
|
|
69
104
|
abi: checkACPValidityAbi,
|
|
70
105
|
functionName: 'checkPermissionValidity',
|
|
71
|
-
args: [
|
|
72
|
-
{
|
|
73
|
-
issuer: acp.issuer,
|
|
74
|
-
expiration: BigInt(acp.expiration),
|
|
75
|
-
recipient: acp.recipient,
|
|
76
|
-
revokerData: BigInt(acp.revokerData),
|
|
77
|
-
revokerContract: acp.revokerContract,
|
|
78
|
-
scope: acp.scope,
|
|
79
|
-
contracts: acp.contracts,
|
|
80
|
-
handles: acp.handles,
|
|
81
|
-
sealingKey: acp.sealingKey,
|
|
82
|
-
issuerSignature: acp.issuerSignature,
|
|
83
|
-
recipientSignature: acp.recipientSignature,
|
|
84
|
-
},
|
|
85
|
-
],
|
|
106
|
+
args: [toACPTuple(acp)],
|
|
86
107
|
});
|
|
87
108
|
return true;
|
|
88
109
|
} catch (err: any) {
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
}
|
|
110
|
+
const errorName = revertErrorName(err, checkACPValidityAbi);
|
|
111
|
+
if (errorName !== undefined) throw new Error(errorName);
|
|
112
|
+
// Fallback throw the original error
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
/** The ACL permission reverts, as statuses. */
|
|
118
|
+
const PERMISSION_ERROR_STATUS: Record<string, ACPAccessStatus> = {
|
|
119
|
+
PermissionInvalid_Expired: 'expired',
|
|
120
|
+
PermissionInvalid_Disabled: 'revoked',
|
|
121
|
+
PermissionInvalid_IssuerSignature: 'invalid-issuer-signature',
|
|
122
|
+
PermissionInvalid_RecipientSignature: 'invalid-recipient-signature',
|
|
123
|
+
};
|
|
103
124
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
125
|
+
/**
|
|
126
|
+
* An ACP status on chain, read from the ACL: its validity (expiration, signatures, revocation)
|
|
127
|
+
* and, given a handle, whether the ACP may read it. The ACL reverts for an invalid ACP; those
|
|
128
|
+
* reverts come back as statuses. Anything else (e.g. a network error) still throws.
|
|
129
|
+
*/
|
|
130
|
+
export const getACPAccessStatusOnChain = async (
|
|
131
|
+
acp: ACPPublic,
|
|
132
|
+
publicClient: PublicClient,
|
|
133
|
+
handle?: bigint | Hex
|
|
134
|
+
): Promise<ACPAccessStatus> => {
|
|
135
|
+
const aclAddress = await getAclAddress(publicClient);
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
try {
|
|
138
|
+
if (handle === undefined) {
|
|
139
|
+
await publicClient.simulateContract({
|
|
140
|
+
address: aclAddress,
|
|
141
|
+
abi: acpAccessAbi,
|
|
142
|
+
functionName: 'checkPermissionValidity',
|
|
143
|
+
args: [toACPTuple(acp)],
|
|
144
|
+
});
|
|
145
|
+
return 'valid';
|
|
113
146
|
}
|
|
114
147
|
|
|
115
|
-
|
|
148
|
+
const ctHash = BigInt(handle);
|
|
149
|
+
const allowed = await publicClient.readContract({
|
|
150
|
+
address: aclAddress,
|
|
151
|
+
abi: acpAccessAbi,
|
|
152
|
+
functionName: 'isAllowedWithPermission',
|
|
153
|
+
args: [toACPTuple(acp), ctHash],
|
|
154
|
+
});
|
|
155
|
+
if (allowed) return 'allowed';
|
|
156
|
+
|
|
157
|
+
// The ACL answers false both when the scope misses the handle and when the issuer itself may
|
|
158
|
+
// not read it; the issuer own allowance tells the two apart.
|
|
159
|
+
const issuerAllowed = await publicClient.readContract({
|
|
160
|
+
address: aclAddress,
|
|
161
|
+
abi: acpAccessAbi,
|
|
162
|
+
functionName: 'isAllowed',
|
|
163
|
+
args: [ctHash, acp.issuer],
|
|
164
|
+
});
|
|
165
|
+
return issuerAllowed ? 'out-of-scope' : 'issuer-not-allowed';
|
|
166
|
+
} catch (err) {
|
|
167
|
+
const status = PERMISSION_ERROR_STATUS[revertErrorName(err, acpAccessAbi) ?? ''];
|
|
168
|
+
if (status) return status;
|
|
116
169
|
throw err;
|
|
117
170
|
}
|
|
118
171
|
};
|
|
@@ -143,7 +196,10 @@ function extractReturnData(err: unknown): `0x${string}` | undefined {
|
|
|
143
196
|
const anyErr = err as any;
|
|
144
197
|
const s = anyErr?.details ?? anyErr?.cause?.details ?? anyErr?.shortMessage ?? anyErr?.message ?? String(err);
|
|
145
198
|
|
|
146
|
-
|
|
199
|
+
// Hardhat: "return data: 0x...". A node that reports only the selector in text (e.g. anvil through
|
|
200
|
+
// a transport that drops the structured error data): "custom error 0x...".
|
|
201
|
+
const match = s.match(/return data:\s*(0x[a-fA-F0-9]+)/) ?? s.match(/custom error (0x[a-fA-F0-9]{8,})/);
|
|
202
|
+
return match?.[1] as `0x${string}` | undefined;
|
|
147
203
|
}
|
|
148
204
|
|
|
149
205
|
const checkACPValidityAbi = [
|
|
@@ -244,3 +300,86 @@ const checkACPValidityAbi = [
|
|
|
244
300
|
inputs: [],
|
|
245
301
|
},
|
|
246
302
|
] as const;
|
|
303
|
+
|
|
304
|
+
const acpAccessAbi = [
|
|
305
|
+
...checkACPValidityAbi,
|
|
306
|
+
{
|
|
307
|
+
type: 'function',
|
|
308
|
+
name: 'isAllowedWithPermission',
|
|
309
|
+
inputs: [checkACPValidityAbi[0].inputs[0], { name: 'handle', type: 'uint256', internalType: 'uint256' }],
|
|
310
|
+
outputs: [{ name: '', type: 'bool', internalType: 'bool' }],
|
|
311
|
+
stateMutability: 'view',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
type: 'function',
|
|
315
|
+
name: 'isAllowed',
|
|
316
|
+
inputs: [
|
|
317
|
+
{ name: 'handle', type: 'uint256', internalType: 'uint256' },
|
|
318
|
+
{ name: 'account', type: 'address', internalType: 'address' },
|
|
319
|
+
],
|
|
320
|
+
outputs: [{ name: '', type: 'bool', internalType: 'bool' }],
|
|
321
|
+
stateMutability: 'view',
|
|
322
|
+
},
|
|
323
|
+
] as const;
|
|
324
|
+
|
|
325
|
+
// ACL-SERVED ADDRESSES (defaultRevokerContract / shareRegistry)
|
|
326
|
+
|
|
327
|
+
const ACL_SERVED_ADDRESSES_ABI = parseAbi([
|
|
328
|
+
'function acl() view returns (address)',
|
|
329
|
+
'function defaultRevokerContract() view returns (address)',
|
|
330
|
+
'function shareRegistry() view returns (address)',
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
export interface AclServedAddresses {
|
|
334
|
+
defaultRevoker?: Hex;
|
|
335
|
+
shareRegistry?: Hex;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const aclServedAddressesCache = new Map<number, AclServedAddresses>();
|
|
339
|
+
|
|
340
|
+
/** Test hook: forget resolved addresses (e.g. between redeployments on one chainId). */
|
|
341
|
+
export const clearAclServedAddresses = () => aclServedAddressesCache.clear();
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The ACP infrastructure addresses the chain's ACL serves (CoFHEAddressBook ->
|
|
345
|
+
* TaskManager -> acl() -> getters). Zero addresses and pre-upgrade ACLs (getters absent -> revert)
|
|
346
|
+
* resolve to `undefined` — callers fall back to `acp.*` config.
|
|
347
|
+
*
|
|
348
|
+
* Resolutions are cached per chainId. A failure to reach the TaskManager (network
|
|
349
|
+
* error, no CoFHE deployment) is NOT cached, so a transient outage does not pin
|
|
350
|
+
* an empty result for the whole session.
|
|
351
|
+
*/
|
|
352
|
+
export const getAclServedAddresses = async (
|
|
353
|
+
publicClient: PublicClient,
|
|
354
|
+
chainId: number
|
|
355
|
+
): Promise<AclServedAddresses> => {
|
|
356
|
+
const cached = aclServedAddressesCache.get(chainId);
|
|
357
|
+
if (cached != null) return cached;
|
|
358
|
+
|
|
359
|
+
let aclAddress: Hex;
|
|
360
|
+
try {
|
|
361
|
+
aclAddress = await publicClient.readContract({
|
|
362
|
+
address: await getTaskManagerAddress(publicClient, chainId),
|
|
363
|
+
abi: ACL_SERVED_ADDRESSES_ABI,
|
|
364
|
+
functionName: 'acl',
|
|
365
|
+
});
|
|
366
|
+
} catch {
|
|
367
|
+
return {};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const [defaultRevoker, shareRegistry] = await Promise.all([
|
|
371
|
+
publicClient
|
|
372
|
+
.readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName: 'defaultRevokerContract' })
|
|
373
|
+
.catch(() => undefined),
|
|
374
|
+
publicClient
|
|
375
|
+
.readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName: 'shareRegistry' })
|
|
376
|
+
.catch(() => undefined),
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
const resolved: AclServedAddresses = {
|
|
380
|
+
defaultRevoker: defaultRevoker != null && defaultRevoker !== zeroAddress ? defaultRevoker : undefined,
|
|
381
|
+
shareRegistry: shareRegistry != null && shareRegistry !== zeroAddress ? shareRegistry : undefined,
|
|
382
|
+
};
|
|
383
|
+
aclServedAddressesCache.set(chainId, resolved);
|
|
384
|
+
return resolved;
|
|
385
|
+
};
|
package/acps/registry.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { encodeAbiParameters, keccak256, parseAbi, type AbiParameterToPrimitiveType, type Hex } from 'viem';
|
|
2
|
+
import { ACPUtils } from './acp.js';
|
|
3
|
+
import type { ACP } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The on-chain sharing pieces: the share registry and revoker ABIs, the registry payload of a
|
|
7
|
+
* sharing ACP and its share id. Exported so an app can send these writes through its own
|
|
8
|
+
* transaction path and still agree with the registry on ids.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ACP default revoker (timestamp-based revocation) — interface shared by all revokers
|
|
12
|
+
export const ACP_REVOKER_ABI = parseAbi([
|
|
13
|
+
'function revokeSingle(uint256 id)',
|
|
14
|
+
'function revokeAllExisting()',
|
|
15
|
+
'function disabled(address issuer, uint256 id) view returns (bool)',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
// SHARE (on-chain, via the ACPShareRegistry)
|
|
19
|
+
|
|
20
|
+
export const ACP_SHARE_REGISTRY_ABI = parseAbi([
|
|
21
|
+
'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
|
+
'function share(ACP calldata acp) external returns (bytes32)',
|
|
23
|
+
'function removeShare(bytes32 shareId) external',
|
|
24
|
+
'function sharesFor(address recipient) external view returns (ACP[] memory)',
|
|
25
|
+
'function getShare(bytes32 shareId) external view returns (ACP memory)',
|
|
26
|
+
'function isShareValid(bytes32 shareId) external view returns (bool)',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const ACP_SHARE_TUPLE_ABI = [
|
|
30
|
+
{
|
|
31
|
+
type: 'tuple',
|
|
32
|
+
components: [
|
|
33
|
+
{ name: 'issuer', type: 'address' },
|
|
34
|
+
{ name: 'expiration', type: 'uint64' },
|
|
35
|
+
{ name: 'recipient', type: 'address' },
|
|
36
|
+
{ name: 'revokerData', type: 'uint256' },
|
|
37
|
+
{ name: 'revokerContract', type: 'address' },
|
|
38
|
+
{ name: 'scope', type: 'uint8' },
|
|
39
|
+
{ name: 'contracts', type: 'address[]' },
|
|
40
|
+
{ name: 'handles', type: 'bytes32[]' },
|
|
41
|
+
{ name: 'sealingKey', type: 'bytes32' },
|
|
42
|
+
{ name: 'issuerSignature', type: 'bytes' },
|
|
43
|
+
{ name: 'recipientSignature', type: 'bytes' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
const ZERO_BYTES32 = `0x${'0'.repeat(64)}` as Hex;
|
|
49
|
+
|
|
50
|
+
/** The on-chain payload for a sharing ACP: recipient-side fields empty. */
|
|
51
|
+
export const toChainShare = (acp: ACP) => {
|
|
52
|
+
// Same public struct as the off-chain export flow (ACPUtils.getPublic), with
|
|
53
|
+
// the recipient-side fields blanked — the recipient supplies them at import —
|
|
54
|
+
// and uint fields widened for the ABI encoder.
|
|
55
|
+
const pub = ACPUtils.getPublic(acp, true);
|
|
56
|
+
return {
|
|
57
|
+
...pub,
|
|
58
|
+
expiration: BigInt(pub.expiration),
|
|
59
|
+
revokerData: BigInt(pub.revokerData),
|
|
60
|
+
sealingKey: ZERO_BYTES32,
|
|
61
|
+
recipientSignature: '0x' as Hex,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Mirrors the registry's `keccak256(abi.encode(acp))` share id. */
|
|
66
|
+
export const computeShareId = (acp: ACP): Hex => {
|
|
67
|
+
const p = toChainShare(acp);
|
|
68
|
+
return keccak256(encodeAbiParameters(ACP_SHARE_TUPLE_ABI, [p]));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** The share id of a registry payload as read back from `sharesFor` / `getShare`. */
|
|
72
|
+
export const shareIdOfChainShare = (share: AbiParameterToPrimitiveType<(typeof ACP_SHARE_TUPLE_ABI)[0]>): Hex =>
|
|
73
|
+
keccak256(encodeAbiParameters(ACP_SHARE_TUPLE_ABI, [share]));
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { PublicClient } from 'viem';
|
|
3
|
+
import { checkACPValidityOnChain, getACPAccessStatusOnChain } from '../onchain-utils';
|
|
4
|
+
import type { ACPPublic } from '../types';
|
|
5
|
+
|
|
6
|
+
const TASK_MANAGER = '0x0000000000000000000000000000000000005000';
|
|
7
|
+
const ACL = '0x00000000000000000000000000000000000000ac';
|
|
8
|
+
const ISSUER = '0x00000000000000000000000000000000000000b0';
|
|
9
|
+
|
|
10
|
+
const acp: ACPPublic = {
|
|
11
|
+
issuer: ISSUER,
|
|
12
|
+
expiration: 2_000_000_000,
|
|
13
|
+
recipient: '0x00000000000000000000000000000000000000a1',
|
|
14
|
+
revokerData: 1,
|
|
15
|
+
revokerContract: '0x00000000000000000000000000000000000000cc',
|
|
16
|
+
scope: 2,
|
|
17
|
+
contracts: [],
|
|
18
|
+
handles: [],
|
|
19
|
+
sealingKey: `0x${'0'.repeat(64)}`,
|
|
20
|
+
issuerSignature: '0x01',
|
|
21
|
+
recipientSignature: '0x02',
|
|
22
|
+
} as unknown as ACPPublic;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A revert as a viem copy OTHER than the SDK one reports it: the same shape, but no class
|
|
26
|
+
* identity the SDK could match with `instanceof`.
|
|
27
|
+
*/
|
|
28
|
+
function foreignRevert(errorName: string) {
|
|
29
|
+
const reverted = { name: 'ContractFunctionRevertedError', data: { errorName } };
|
|
30
|
+
return Object.assign(new Error(`The contract function reverted: ${errorName}`), {
|
|
31
|
+
walk: (fn: (e: unknown) => boolean) => (fn(reverted) ? reverted : null),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function client({ simulate, reads = {} }: { simulate?: () => unknown; reads?: Record<string, () => unknown> }) {
|
|
36
|
+
return {
|
|
37
|
+
chain: { id: 31337 },
|
|
38
|
+
readContract: vi.fn(async ({ functionName }: { functionName: string }) => {
|
|
39
|
+
if (functionName === 'getTm') return TASK_MANAGER;
|
|
40
|
+
if (functionName === 'acl') return ACL;
|
|
41
|
+
const read = reads[functionName];
|
|
42
|
+
if (!read) throw new Error(`unexpected read ${functionName}`);
|
|
43
|
+
return read();
|
|
44
|
+
}),
|
|
45
|
+
simulateContract: vi.fn(async () => simulate?.()),
|
|
46
|
+
} as unknown as PublicClient;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('getACPAccessStatusOnChain', () => {
|
|
50
|
+
it('returns valid when the ACL accepts the ACP', async () => {
|
|
51
|
+
expect(await getACPAccessStatusOnChain(acp, client({}))).toBe('valid');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
['PermissionInvalid_Expired', 'expired'],
|
|
56
|
+
['PermissionInvalid_Disabled', 'revoked'],
|
|
57
|
+
['PermissionInvalid_IssuerSignature', 'invalid-issuer-signature'],
|
|
58
|
+
['PermissionInvalid_RecipientSignature', 'invalid-recipient-signature'],
|
|
59
|
+
])('maps %s to %s, even from another viem copy', async (errorName, status) => {
|
|
60
|
+
const publicClient = client({
|
|
61
|
+
simulate: () => {
|
|
62
|
+
throw foreignRevert(errorName);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
expect(await getACPAccessStatusOnChain(acp, publicClient)).toBe(status);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('tells out-of-scope from issuer-not-allowed with the issuer own allowance', async () => {
|
|
69
|
+
const outOfScope = client({ reads: { isAllowedWithPermission: () => false, isAllowed: () => true } });
|
|
70
|
+
const notAllowed = client({ reads: { isAllowedWithPermission: () => false, isAllowed: () => false } });
|
|
71
|
+
const allowed = client({ reads: { isAllowedWithPermission: () => true } });
|
|
72
|
+
expect(await getACPAccessStatusOnChain(acp, outOfScope, 1n)).toBe('out-of-scope');
|
|
73
|
+
expect(await getACPAccessStatusOnChain(acp, notAllowed, '0x01')).toBe('issuer-not-allowed');
|
|
74
|
+
expect(await getACPAccessStatusOnChain(acp, allowed, 1n)).toBe('allowed');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('maps a revert on the handle check too', async () => {
|
|
78
|
+
const publicClient = client({
|
|
79
|
+
reads: {
|
|
80
|
+
isAllowedWithPermission: () => {
|
|
81
|
+
throw foreignRevert('PermissionInvalid_Disabled');
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
expect(await getACPAccessStatusOnChain(acp, publicClient, 1n)).toBe('revoked');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('reads a revert reported only as text with the error selector', async () => {
|
|
89
|
+
const publicClient = client({
|
|
90
|
+
simulate: () => {
|
|
91
|
+
throw new Error('Execution reverted with reason: RPC eth_call failed: custom error 0xcbd3a966.');
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
expect(await getACPAccessStatusOnChain(acp, publicClient)).toBe('revoked');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('still throws what is not an ACL revert', async () => {
|
|
98
|
+
const publicClient = client({
|
|
99
|
+
simulate: () => {
|
|
100
|
+
throw new Error('fetch failed');
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
await expect(getACPAccessStatusOnChain(acp, publicClient)).rejects.toThrow('fetch failed');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('checkACPValidityOnChain', () => {
|
|
108
|
+
it('names the revert from another viem copy instead of rethrowing it raw', async () => {
|
|
109
|
+
const publicClient = client({
|
|
110
|
+
simulate: () => {
|
|
111
|
+
throw foreignRevert('PermissionInvalid_Disabled');
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
await expect(checkACPValidityOnChain(acp, publicClient)).rejects.toThrow(/^PermissionInvalid_Disabled$/);
|
|
115
|
+
});
|
|
116
|
+
});
|
package/acps/types.ts
CHANGED
|
@@ -221,6 +221,22 @@ export type SharedACP = Expand<
|
|
|
221
221
|
}
|
|
222
222
|
>;
|
|
223
223
|
|
|
224
|
+
/**
|
|
225
|
+
* An ACP on-chain status, as `client.acp.checkAccess` reports it instead of a revert.
|
|
226
|
+
* Without a handle: 'valid' or the reason it is not. With a handle, a valid ACP reports
|
|
227
|
+
* 'allowed', 'out-of-scope' (the ACP scope does not cover the handle) or
|
|
228
|
+
* 'issuer-not-allowed' (the issuer itself may not read the handle, so no ACP of theirs can).
|
|
229
|
+
*/
|
|
230
|
+
export type ACPAccessStatus =
|
|
231
|
+
| 'valid'
|
|
232
|
+
| 'allowed'
|
|
233
|
+
| 'out-of-scope'
|
|
234
|
+
| 'issuer-not-allowed'
|
|
235
|
+
| 'expired'
|
|
236
|
+
| 'revoked'
|
|
237
|
+
| 'invalid-issuer-signature'
|
|
238
|
+
| 'invalid-recipient-signature';
|
|
239
|
+
|
|
224
240
|
/**
|
|
225
241
|
* A share read back from the on-chain ACPShareRegistry: the posted payload
|
|
226
242
|
* (SharedACP minus the client-side name/type) plus its registry id.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineChain } from '../defineChain.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Arbitrum One mainnet chain configuration
|
|
5
|
+
*/
|
|
6
|
+
export const arbitrum = defineChain({
|
|
7
|
+
id: 42161,
|
|
8
|
+
name: 'Arbitrum One',
|
|
9
|
+
network: 'arbitrum',
|
|
10
|
+
coFheUrl: 'https://cofhe.mainnet.fhenix.zone',
|
|
11
|
+
verifierUrl: 'https://cofhe-vrf.mainnet.fhenix.zone',
|
|
12
|
+
thresholdNetworkUrl: 'https://cofhe-tn.mainnet.fhenix.zone',
|
|
13
|
+
environment: 'MAINNET',
|
|
14
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineChain } from '../defineChain.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ethereum mainnet chain configuration
|
|
5
|
+
*/
|
|
6
|
+
export const ethereum = defineChain({
|
|
7
|
+
id: 1,
|
|
8
|
+
name: 'Ethereum',
|
|
9
|
+
network: 'ethereum',
|
|
10
|
+
coFheUrl: 'https://cofhe.mainnet.fhenix.zone',
|
|
11
|
+
verifierUrl: 'https://cofhe-vrf.mainnet.fhenix.zone',
|
|
12
|
+
thresholdNetworkUrl: 'https://cofhe-tn.mainnet.fhenix.zone',
|
|
13
|
+
environment: 'MAINNET',
|
|
14
|
+
});
|
package/chains/index.ts
CHANGED
|
@@ -5,17 +5,21 @@ export type { CofheChain, Environment } from './types.js';
|
|
|
5
5
|
import { sepolia } from './chains/sepolia.js';
|
|
6
6
|
import { arbSepolia } from './chains/arbSepolia.js';
|
|
7
7
|
import { baseSepolia } from './chains/baseSepolia.js';
|
|
8
|
+
import { ethereum } from './chains/ethereum.js';
|
|
9
|
+
import { arbitrum } from './chains/arbitrum.js';
|
|
8
10
|
import { hardhat } from './chains/hardhat.js';
|
|
9
11
|
import { localcofhe } from './chains/localcofhe.js';
|
|
10
12
|
import { stagingCofhe } from './chains/stagingCofhe.js';
|
|
11
13
|
|
|
12
|
-
export { sepolia, arbSepolia, baseSepolia, hardhat, localcofhe, stagingCofhe };
|
|
14
|
+
export { sepolia, arbSepolia, baseSepolia, ethereum, arbitrum, hardhat, localcofhe, stagingCofhe };
|
|
13
15
|
|
|
14
16
|
// Export all chains as a collection
|
|
15
17
|
export const chains = {
|
|
16
18
|
sepolia,
|
|
17
19
|
arbSepolia,
|
|
18
20
|
baseSepolia,
|
|
21
|
+
ethereum,
|
|
22
|
+
arbitrum,
|
|
19
23
|
hardhat,
|
|
20
24
|
localcofhe,
|
|
21
25
|
stagingCofhe,
|