Skip to content

Latest commit

 

History

History
91 lines (64 loc) · 3.97 KB

File metadata and controls

91 lines (64 loc) · 3.97 KB
title description
Encrypt Inputs
How to encrypt values and generate input proofs for FHEVM contract interactions.

Encrypt Inputs

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.

How It Works

When you call an encrypt* function, forge-fhevm:

  1. Derives a deterministic ciphertext handle from the value, nonce, and FHE type
  2. Stores the plaintext in an internal mapping (for later decryption in tests)
  3. Computes an EIP-712 digest over the handle, user address, and target contract
  4. Signs the digest with a mock signer key using vm.sign()
  5. 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. :::

Basic Encryption

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. :::

Supported Types

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.

Encrypting for a Different User

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);

Handle Uniqueness

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));