Skip to main content
An Access Control Permission (ACP) is an EIP-712 signature that authorizes decryption of confidential data. The issuer field identifies who is reading the data, and that address must already have been granted access onchain with FHE.allow(handle, address). When you use an ACP, CoFHE validates it against the ACL contract to confirm the issuer really holds that access. Every ACP carries a sealing keypair. The public key goes to CoFHE so it can re-encrypt the result for the holder. The private key stays on the client and unseals the value when it comes back.
ACPs were called permits before 0.7. The name changed so they are not confused with an ERC-2612 permit, which is a different thing entirely. If you are upgrading, see migrating to 0.7. This page uses “ACP” throughout.

When you need one

  • decryptForView always requires an ACP.
  • decryptForTx depends on the contract’s ACL policy for that handle. If the policy lets anyone decrypt, use .withoutACP(). If it restricts decryption, use .withACP(...).

Prerequisites

Create and connect a client. ACPs are scoped to a chainId and account pair.

Quick start

client.acp is the recommended API. It signs with the connected wallet and manages the store for you. ACPUtils is the lower-level alternative when you want direct control over signing and storage. Note the namespace is singular: client.acp, not client.acps.
After this the active ACP is picked up automatically by decryptForView(...).execute() and by decryptForTx(...).withACP().execute().

The three types

expiration is a unix timestamp in seconds and defaults to 7 days from creation. The client-wide defaultACPExpiration config key is a separate setting that defaults to 30 days. Creating an ACP through client.acp.* stores it and makes it active.

Creating a self ACP

A self ACP lets you decrypt data that was allowed to your own address.
createSelf always creates a new one. getOrCreateSelfACP() reuses the active ACP when there is one, which is what most applications want.

Narrowing what an ACP can read

0.7 adds a scope to every ACP. An unscoped ACP covers everything the issuer can read, which is rarely what you want to hand to someone else.
A scope only ever narrows the issuer’s existing access. It cannot grant access the issuer does not already hold, so scoping is not a way to delegate something you were never allowed to read. It also does not retroactively narrow ACPs you already issued.
Handles are bytes32 hex strings here, not bigints. If you are carrying handle values around as bigints, convert before putting them in an ACP.

Sharing with another account

An issuer can delegate their ACL access to a recipient, who can then decrypt the issuer’s data without holding their own FHE.allow grant. There are two routes: pass the offer yourself, or post it onchain.

Passing the offer yourself

1

Issuer creates a sharing ACP

2

Issuer exports it

The exported JSON holds no sensitive data and can travel over any channel.
ACPUtils.export throws unless the ACP is a signed sharing ACP. In 0.6 the equivalent call serialized anything you gave it. A call that used to always work, such as one made during a render, now always throws on a self ACP. Gate it on acp.type === 'sharing'.Never share the output of serialize(acp). That is for local persistence and contains the sealing private key.
3

Recipient imports and signs

Importing generates a fresh sealing key for the recipient.

Sharing onchain

The issuer can instead post the signed offer to a registry, so the recipient discovers it without a side channel.
dismissShare(shareId) clears an entry the recipient does not want, and cancelShare withdraws one the issuer posted.

Revoking access

An issuer can revoke an ACP they created, which matters when a sharing ACP has left their control.
Revocation is checked when the ACP is used, so it applies to copies the issuer no longer holds.

Managing stored ACPs

The SDK keeps every stored ACP and one active ACP hash per chainId and account.

Validating

ACPUtils.validate enforces the full check: schema, signed, and not expired. The decrypt flows call it for you and surface failures as typed errors, so validate manually only when you want to inspect or filter ACPs first. Use validateSchema on an ACP that arrived over the wire, when you want to reject a malformed payload before caring about expiry. For inspection without exception handling, ValidationUtils returns a typed result:
Match on result.error to render a precise message:

Persistence and security

  • ACPs are stored per chainId and account. On the web the store is localStorage under the key cofhesdk-acps.
  • A stored ACP contains the sealing private key. Treat it as a secret, and never hand a serialized ACP to another user.
  • To share access, use ACPUtils.export, which strips the sensitive fields.
Permits stored by 0.6 are dropped on upgrade. They were signed with EIP-712 types the upgraded ACL no longer accepts, so they cannot verify and are discarded when the store loads. Your users are prompted to sign again. Nothing needs migrating, but it looks like data loss if you are not expecting it.