| title | description |
|---|---|
Encrypt Inputs |
How to encrypt values and generate input proofs for FHEVM contract interactions. |
This guide covers how to encrypt plaintext values into FHE handles with valid input proofs. You use the encrypt* family of functions provided by FhevmTest.
When you call an encrypt* function, forge-fhevm:
- Derives a deterministic ciphertext handle from the value, nonce, and FHE type
- Stores the plaintext in an internal mapping (for later decryption in tests)
- Computes an EIP-712 digest over the handle, user address, and target contract
- Signs the digest with a mock signer key using
vm.sign() - Assembles the signed input proof in the wire format expected by
InputVerifier
The returned handle and proof can be passed directly to any contract that calls FHE.fromExternal().
::: info No real proofs needed for testing
In production, input proofs contain cryptographic attestations generated by the Zama infrastructure. In a testing context, the InputVerifier only checks that the proof carries a valid EIP-712 signature from a trusted signer — no actual ZK proof is required.
forge-fhevm signs proofs with a mock key that the InputVerifier is configured to trust. As a smart contract developer, you never need to worry about proof validity — your contract calls FHE.fromExternal(handle, proof) and the protocol handles verification. This is the same in tests and in production.
:::
Every encrypt* function has two overloads:
- Two-argument — uses
address(this)(the test contract) as both user and caller - Three-argument — lets you specify an explicit user address and target contract
// Two-argument: test contract is the implicit user
(externalEuint64 handle, bytes memory proof) = encryptUint64(42, address(myContract));
// Three-argument: specify a different user
address alice = address(0xA11CE);
(externalEuint64 handle, bytes memory proof) = encryptUint64(42, alice, address(myContract));::: warning
The target address must match the contract that will call FHE.fromExternal(). If the addresses don't match, the InputVerifier will reject the proof.
:::
| Function | Value Type | FHE Type | Return Handle |
|---|---|---|---|
encryptBool |
bool |
FheType.Bool |
externalEbool |
encryptUint8 |
uint8 |
FheType.Uint8 |
externalEuint8 |
encryptUint16 |
uint16 |
FheType.Uint16 |
externalEuint16 |
encryptUint32 |
uint32 |
FheType.Uint32 |
externalEuint32 |
encryptUint64 |
uint64 |
FheType.Uint64 |
externalEuint64 |
encryptUint128 |
uint128 |
FheType.Uint128 |
externalEuint128 |
encryptUint256 |
uint256 |
FheType.Uint256 |
externalEuint256 |
encryptAddress |
address |
FheType.Uint160 |
externalEaddress |
Every function returns (externalE*, bytes memory inputProof).
All encrypted types share the same 32-byte handle structure. There is no way to distinguish handles by looking at them — the input proof is what ties a handle to a specific user and target contract.
When testing user-facing flows (e.g., a user minting tokens), encrypt with the user's address and use vm.prank:
address alice = address(0xA11CE);
SampleEncryptedToken token = new SampleEncryptedToken();
// Encrypt as Alice, targeting the token contract
(externalEuint64 amount, bytes memory proof) = encryptUint64(100, alice, address(token));
// Call mint as Alice
vm.prank(alice);
token.mint(amount, proof);Each call to encrypt* increments an internal nonce. Encrypting the same value twice produces different handles:
(externalEuint64 first,) = encryptUint64(42, address(this));
(externalEuint64 second,) = encryptUint64(42, address(this));
// Different handles, same plaintext
assertNotEq(externalEuint64.unwrap(first), externalEuint64.unwrap(second));