| name | fhevm-decryption |
| description | Used when implementing or debugging FHEVM decryption flows — user decryption (sdk.userDecrypt with the signer/keypair bound at instance creation) and public decryption (v0.9 three-step pattern: FHE.makePubliclyDecryptable on-chain → off-chain sdk.publicDecrypt → on-chain FHE.checkSignatures). Targets `@zama-fhe/sdk@3.0.0` (provider-agnostic; relayer-sdk is a transitive dep, not a direct install). Explicitly covers the removal of the deprecated FHE.requestDecryption oracle-callback API. Triggers on prompts mentioning userDecrypt, publicDecrypt, makePubliclyDecryptable, checkSignatures, EIP-712 decryption, reencrypt (legacy term), or the v0.9 migration of decryption. |
fhevm-decryption
FHEVM decryption is always async and multi-step. There is no synchronous on-chain decrypt(). Two patterns — user decryption (EIP-712 re-encryption, scoped to a single user) and public decryption (three-step reveal, world-readable) — cover every use case. The pre-v0.9 FHE.requestDecryption oracle-callback API has been removed.
The full on-chain-snippet / TypeScript-integration / ABI-encoding reference lives in references/decryption-patterns.md. This file is the routing core.
Essential Principles
1. There is no on-chain decrypt()
Nothing on the host chain ever produces a plaintext synchronously. Every decryption path ends with a cleartext held by the client, optionally brought back on-chain with a verified KMS threshold signature. Agents that emit bytes memory plaintext = FHE.decrypt(handle) are wrong every time.
2. FHE.requestDecryption is removed — do not use it
The pre-v0.9 pattern uint256 reqId = FHE.requestDecryption(handles, this.callback.selector) is no longer exported from @fhevm/solidity. Any example showing it is at least a year out of date. See fhevm-antipatterns entry 3.
3. Public vs user decryption — pick by who may read the value
publicDecrypt — the value is world-readable. On-chain FHE.makePubliclyDecryptable(handle) is permanent and irreversible; the cleartext can then be fetched by anyone via the relayer and brought back on-chain with a KMS threshold signature that FHE.checkSignatures verifies.
userDecrypt — the value is scoped to a single EOA. On-chain FHE.allow(handle, user) grants the ACL; the user signs an EIP-712 permit authorizing the relayer to re-encrypt the handle under the user's ephemeral ML-KEM public key. The cleartext never leaves the browser.
Using publicDecrypt for a per-user value leaks it. Using userDecrypt for a public value blocks other participants from verifying it.
4. Proof order is cryptographically bound
The KMS threshold signature returned by publicDecrypt([h1, h2]) is bound to the handle order. abi.encode(cleartext1, cleartext2) on-chain must match that order exactly — swapping the pair makes FHE.checkSignatures revert with EmptyDecryptionProof() or similar. Never reorder handles between the off-chain call and the on-chain checkSignatures.
5. Before makePubliclyDecryptable, the contract needs ACL on the handle
FHE.makePubliclyDecryptable(h) requires the caller to already hold an ACL grant on h. Derived handles from FHE.* ops carry no default permissions (see fhevm-contracts Essential Principle 4). Call FHE.allowThis(h) before makePubliclyDecryptable(h) if the handle is a fresh op result.
Public decryption — three-step flow
// On-chain (state-mutating tx)
FHE.allowThis(_tally); // contract must hold ACL first
FHE.makePubliclyDecryptable(_tally);
emit DecryptionRequested(_tally);
import { ZamaSDK, RelayerNode, SepoliaConfig, MemoryStorage } from "@zama-fhe/sdk";
import { EthersSigner } from "@zama-fhe/sdk/ethers";
const sdk = new ZamaSDK({
relayer: new RelayerNode({
transports: { 11155111: { ...SepoliaConfig } },
getChainId: async () => 11155111,
}),
signer: new EthersSigner({ signer: ethersWallet }),
storage: new MemoryStorage(),
});
const { clearValues, abiEncodedClearValues, decryptionProof } =
await sdk.publicDecrypt([handle]);
// On-chain (state-mutating finalize tx)
function finalize(uint64 clearTally, bytes calldata proof) external {
bytes32[] memory handles = new bytes32[](1);
handles[0] = euint64.unwrap(_tally);
FHE.checkSignatures(handles, abi.encode(clearTally), proof);
_finalized = true;
_publicTally = clearTally;
}
Full snippets (multi-handle, ABI encoding by type) in references/decryption-patterns.md.
User decryption — EIP-712 flow
// On-chain: grant the user ACL access to the handle (inside the mutating tx)
FHE.allow(_balances[msg.sender], msg.sender);
const result = await sdk.userDecrypt([{ handle, contractAddress }]);
const cleartext = result[handle];
Keypair and permit caching are handled by @zama-fhe/sdk@3.0.0 internally — new ZamaSDK({ relayer, signer, storage }) returns a long-lived SDK that reuses the EIP-712 permit and ML-KEM keypair across calls. For React, the sister package @zama-fhe/react-sdk wires this up via @tanstack/query-core-powered hooks (see fhevm-frontend).
Picking the right flow
| Scenario | Use |
|---|
| Auction clears, the winning price is revealed to everyone | publicDecrypt |
| Vote tallies are revealed after closing | publicDecrypt |
| Liquidation check reveals the under-collateralized amount | publicDecrypt |
| User reads their ERC-7984 balance | userDecrypt |
| User verifies their own vote was counted | userDecrypt |
| User reads their vault position | userDecrypt |
| A bot or contract needs automatic reveal with no client | Neither — FHE.requestDecryption is removed. Design the flow around a settlement bot that runs the three-step public-decrypt itself. |
Cross-references — which contract uses which flow
- User decryption is the right fit for per-user state (private balance, vault position, vote receipt). Pattern: contract
FHE.allow(handle, msg.sender) after every state write; client calls sdk.userDecrypt([{ handle, contractAddress }]) with an EIP-712 signature.
- Public decryption is the right fit for tally / settlement reveals (auction winner, voting result, vault snapshot rate). Pattern: contract
FHE.makePubliclyDecryptable(handle) → off-chain sdk.publicDecrypt([handle]) → on-chain FHE.checkSignatures(...) before trusting the value.
- For full working examples, see Zama's official dApps repo.
fhevm-contracts — writing the contract side (allowThis, makePubliclyDecryptable, checkSignatures).
fhevm-antipatterns entry 3 — full migration guide for codebases still using FHE.requestDecryption.
fhevm-frontend — React hooks (useUserDecrypt, usePublicDecrypt) that wrap this flow.
fhevm-overview — why decryption is async (KMS threshold network, relayer, gateway).
When to Use
- Implementing
makePubliclyDecryptable → publicDecrypt → checkSignatures end-to-end.
- Implementing EIP-712 user decryption —
sdk.userDecrypt([{ handle, contractAddress }]). The signer is bound at new ZamaSDK({ ... }); the SDK handles the keypair + EIP-712 permit internally.
- Debugging
EmptyDecryptionProof() reverts on checkSignatures.
- Migrating legacy code that still calls
FHE.requestDecryption.
- Deciding between public and user decryption for a specific use case.
When NOT to Use
- Writing the encrypted-state logic that produces the handles →
fhevm-contracts.
- Building the browser UI that triggers user decrypt →
fhevm-frontend.
- Testing decryption flows in Mocha / Hardhat mock →
fhevm-testing.
- Understanding why the flow is async at all →
fhevm-overview.
Routing
Which question does the user's prompt match?
- "How do I reveal a value publicly?" → Public decryption section above +
references/decryption-patterns.md.
- "How do I let a user decrypt their own value?" → User decryption section above.
- "What is
FHE.requestDecryption / oracle callback?" → Essential Principle 2 + fhevm-antipatterns entry 3.
- "Why am I getting
EmptyDecryptionProof()?" → Essential Principle 4 (handle order) + references/decryption-patterns.md § ABI Encoding.
- "Can the contract auto-decrypt without a client?" → "Picking the right flow" table — no, write a settlement bot.
- "What's the difference between public and user decryption?" → "Picking the right flow" table above.
External references
See fhevm-overview for the canonical Zama list. Decryption-specific deep dives: