Help us improve
Share bugs, ideas, or general feedback.
From bn-fhevm
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.
npx claudepluginhub bootnodedev/zama-s2-bounty-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/bn-fhevm:fhevm-decryptionThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
Measures whether skills, rules, and agent definitions are actually followed by auto-generating test scenarios at 3 strictness levels and reporting compliance rates with full tool call timelines.
Share bugs, ideas, or general feedback.
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.
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.
FHE.requestDecryption is removed — do not use itThe 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.
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.
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.
makePubliclyDecryptable, the contract needs ACL on the handleFHE.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.
// On-chain (state-mutating tx)
FHE.allowThis(_tally); // contract must hold ACL first
FHE.makePubliclyDecryptable(_tally);
emit DecryptionRequested(_tally);
// Off-chain (Node client) — @zama-fhe/sdk@3.0.0
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 }), // or { ethereum: provider }
storage: new MemoryStorage(),
});
const { clearValues, abiEncodedClearValues, decryptionProof } =
await sdk.publicDecrypt([handle]);
// clearValues[handle] — bigint | boolean | address
// abiEncodedClearValues — bytes passed to on-chain finalize
// decryptionProof — KMS threshold signature
// 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.
// On-chain: grant the user ACL access to the handle (inside the mutating tx)
FHE.allow(_balances[msg.sender], msg.sender);
// Off-chain (Node client) — @zama-fhe/sdk@3.0.0
// Reuse the SDK instance constructed above; it handles the EIP-712 permit
// and the ephemeral ML-KEM keypair internally. The cleartext never leaves
// the client.
// userDecrypt takes an array of (handle, contractAddress) pairs.
const result = await sdk.userDecrypt([{ handle, contractAddress }]);
const cleartext = result[handle]; // bigint | boolean | Hex
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).
| 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. |
FHE.allow(handle, msg.sender) after every state write; client calls sdk.userDecrypt([{ handle, contractAddress }]) with an EIP-712 signature.FHE.makePubliclyDecryptable(handle) → off-chain sdk.publicDecrypt([handle]) → on-chain FHE.checkSignatures(...) before trusting the value.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).makePubliclyDecryptable → publicDecrypt → checkSignatures end-to-end.sdk.userDecrypt([{ handle, contractAddress }]). The signer is bound at new ZamaSDK({ ... }); the SDK handles the keypair + EIP-712 permit internally.EmptyDecryptionProof() reverts on checkSignatures.FHE.requestDecryption.fhevm-contracts.fhevm-frontend.fhevm-testing.fhevm-overview.references/decryption-patterns.md.FHE.requestDecryption / oracle callback?" → Essential Principle 2 + fhevm-antipatterns entry 3.EmptyDecryptionProof()?" → Essential Principle 4 (handle order) + references/decryption-patterns.md § ABI Encoding.See fhevm-overview for the canonical Zama list. Decryption-specific deep dives: