| name | fhevm-testing |
| description | Used when writing or debugging Mocha / Hardhat tests for FHEVM contracts. Covers the fhevm.isMock gate pattern, encrypted input creation via fhevm.createEncryptedInput + FHE.fromExternal, test-time decryption via fhevm.userDecryptEuint and fhevm.publicDecrypt, EIP-712 PublicDecryptVerification proofs using the mock KMS private key, Sepolia timeout / retry conventions, and the three-gate deployment checklist (mock → Sepolia → pre-mainnet review). Triggers on @fhevm/hardhat-plugin, @fhevm/mock-utils, hre.fhevm, createEncryptedInput, userDecryptEuint, publicDecrypt, EmptyDecryptionProof in tests, or writing a first Hardhat test for an FHEVM contract. |
fhevm-testing
Test patterns for FHEVM contracts under Mocha + Hardhat. The mock coprocessor from @fhevm/hardhat-plugin@0.4.2 runs in-process on the hardhat network; the real protocol runs on Sepolia. A single test file should handle both by gating on hre.fhevm.isMock. The full API catalog with signatures and proof-construction snippets lives in references/testing-guide.md — this file is the routing core.
Essential Principles
1. Test against the mock first, then Sepolia
Default workflow: write tests for mock mode (hre.fhevm.isMock === true), iterate fast, then opt into Sepolia for the same test suite via npm run test:sepolia. Never ship a contract without both passing. Mock catches logic bugs; Sepolia catches real-network issues (relayer timing, KMS signature formats, gas).
2. Gate network-specific assertions on hre.fhevm.isMock
it("reveals the tally", async () => {
if (fhevm.isMock) {
} else {
}
});
A branchless test that calls, e.g., an Express proxy for local mock but a real HTTPS relayer for Sepolia will fail on one or the other.
3. Encrypted inputs use fhevm.createEncryptedInput(contract, user)
The plugin injects fhevm.createEncryptedInput(contractAddress, userAddress) as a helper that produces (handles[], inputProof) bound to (contract, user) — identical semantics to the browser SDK, but in-process. Any test that calls a function taking externalEuint* + bytes proof must use this.
const input = fhevm.createEncryptedInput(contract.target, user.address);
input.add64(42n);
const { handles, inputProof } = await input.encrypt();
await contract.connect(user).increment(handles[0], inputProof);
4. Decryption in tests
Two test helpers match the two production flows — see fhevm-decryption for the protocol view.
- User decryption (per-user):
const clear = await fhevm.userDecryptEuint(FhevmType.euint64, handle, contractAddress, user); — asserts the user has ACL permission on the handle and returns the cleartext. The 4th argument is the user's signer; the 3rd is the contract address that holds the handle.
- Public decryption (world-readable):
const clear = await fhevm.publicDecryptEuint(FhevmType.euint64, handle); — returns the cleartext. (The bare fhevm.publicDecrypt(handles[]) exists too but takes an array and returns a PublicDecryptResults object; use the per-type wrapper publicDecryptEuint for single-handle scalar reads.)
For contract-side FHE.checkSignatures verification in tests, you must sign with the mock KMS private key — see references/testing-guide.md § mock KMS proof construction.
5. EmptyDecryptionProof() in tests → the proof is empty or misbuilt
Mock mode is NOT a no-op. FHE.checkSignatures verifies the EIP-712 signature even in mock tests. Feeding "0x" into a finalize(clear, proof) call fails. Construct a valid proof with the mock KMS key or use fhevm.publicDecrypt which returns a proof alongside the cleartext.
6. The three-gate deployment checklist
Before moving a contract from draft to production, pass three gates:
- Mock gate —
npm test (mock mode) green. Exercises logic, ACL, input-proof flow, both decryption paths.
- Sepolia gate —
npm run test:sepolia green. Same assertions against the real relayer and real KMS. Catches relayer-URL config errors, real-network timing, gas misestimates.
- Pre-mainnet review — code review with
fhevm-antipatterns checklist scanned, slither . clean, and at least one auditor sign-off on FHE.allow* placements and FHE.makePubliclyDecryptable irreversibility.
Skipping gate 2 is the most common shipping mistake — mock mode hides relayer configuration bugs.
Minimal test skeleton
import { expect } from "chai";
import { ethers, fhevm } from "hardhat";
import { FhevmType } from "@fhevm/hardhat-plugin";
import type { ConfidentialCounter } from "../typechain-types";
describe("ConfidentialCounter", () => {
let counter: ConfidentialCounter;
let owner: HardhatEthersSigner, user: HardhatEthersSigner;
beforeEach(async () => {
[owner, user] = await ethers.getSigners();
const Factory = await ethers.getContractFactory("ConfidentialCounter");
counter = await Factory.deploy();
await counter.waitForDeployment();
});
it("increments by an encrypted delta and the user can decrypt", async () => {
const input = fhevm.createEncryptedInput(counter.target, user.address);
input.add64(7n);
const { handles, inputProof } = await input.encrypt();
await counter.connect(user).increment(handles[0], inputProof);
const handle = await counter.countHandle();
const clear = await fhevm.userDecryptEuint(
FhevmType.euint64,
handle,
await counter.getAddress(),
user,
);
expect(clear).to.equal(7n);
});
it("publicly reveals the count after makePubliclyDecryptable", async () => {
const handle = await counter.countHandle();
await counter.requestReveal();
const clear = await fhevm.publicDecryptEuint(FhevmType.euint64, handle);
expect(clear).to.be.a("bigint");
if (fhevm.isMock) {
}
});
});
Full snippets for multi-handle public decryption, encoded-cleartext proof construction, ACL assertions, and Sepolia timeout/retry patterns are in references/testing-guide.md.
Cross-references — canonical test patterns
For full worked examples covering user decryption, ERC-7984 wrap/unwrap roundtrips, and public-decrypt finalization with checkSignatures, see the test suites in Zama's official dApps repo.
Sepolia variants of mock tests typically live in matching test/<Contract>Sepolia.ts files and run via npm run test:sepolia.
When to Use
- Writing a first Mocha test for an FHEVM contract.
- Constructing encrypted inputs in tests (
createEncryptedInput).
- Asserting on decrypted values in tests (
userDecryptEuint, publicDecrypt).
- Signing
PublicDecryptVerification proofs with the mock KMS key.
- Adding a Sepolia-gated test path or debugging why a test passes on mock but fails on Sepolia.
- Deciding whether a contract is ready to ship (three-gate checklist).
When NOT to Use
Routing
Which task does the user's prompt match?
- "Write a first test for my FHEVM contract." → Minimal test skeleton above.
- "How do I build an encrypted input in a test?" → Essential Principle 3 +
references/testing-guide.md § createEncryptedInput.
- "How do I read the cleartext of a handle in a test?" → Essential Principle 4 +
references/testing-guide.md § userDecryptEuint / publicDecrypt.
- "I'm getting
EmptyDecryptionProof() in mock mode." → Essential Principle 5 + references/testing-guide.md § mock KMS proof construction.
- "My test passes on mock but fails on Sepolia." → Essential Principle 2 +
references/testing-guide.md § Sepolia gating.
- "Is my contract ready to ship?" → Essential Principle 6 (three-gate checklist).
Stack versions & external references
Canonical pinned versions and Zama documentation links live in fhevm-overview. Testing-specific extras: @fhevm/hardhat-plugin@0.4.2 (injects hre.fhevm), @fhevm/mock-utils@0.4.2, @nomicfoundation/hardhat-chai-matchers@^2.1.x, chai@^4.5.x, mocha@^11.x. Testing-guide URL: https://docs.zama.org/protocol/solidity-guides/development-guide/testing.