使用 Foundry 编写 FHEVM 智能合约测试:forge-fhevm 完整实战指南
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
本篇技术指南以 fh/fhevm 仓库的 Foundry 测试文档为核心,系统讲解如何基于 forge-fhevm(Foundry 原生的 FHEVM 测试库)编写全同态加密智能合约的单元测试。你将掌握FhevmTest基类继承、encrypt*输入加密助手、三种解密模式(decrypt/publicDecrypt/userDecrypt)以及完整的加密 → 执行 → 解密测试闭环,并可直接用于本仓库中的FHECounter等加密合约项目。
为什么在 Foundry 中测试 FHEVM 合约
FHEVM 合约与普通 Solidity 合约的关键区别在于:合约内部存储与操作的是密文句柄(handle),而非明文值;调用方必须为每个加密输入附带 EIP-712 格式的输入证明(input proof),由链上的InputVerifier验证后才能执行FHE.fromExternal。这些逻辑无法用传统 Mock 测试覆盖,必须在测试环境中真实部署 FHEVM 主网合约(host contracts)。
forge-fhevm解决这一问题的思路是:把 FHEVM 的 host contracts——FHEVMExecutor、ACL、InputVerifier、KMSVerifier——直接部署进 Foundry 的测试 EVM 中,让被测合约运行与生产环境完全相同的链上代码路径:输入验证、ACL 权限强制、句柄生命周期全部按主网行为执行。唯一的差异是 FHE 协处理器计算被模拟:明文值在本地跟踪,因此测试里可以直接对解密结果做assertEq断言(参见 Foundry 指南 README)。
同时,测试环境与主网唯一的区别在于使用了模拟私钥(mock input signer 与 mock KMS signer),从而保证测试中 EIP-712 证明的确定性生成。
继承 FhevmTest 基类
每个 FHEVM 测试合约都必须继承FhevmTest。在setUp()中调用super.setUp()会将 FHEVM host contracts 部署到它们的规范确定性地址(canonical deterministic addresses)上:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.27; import {FhevmTest} from "forge-fhevm/FhevmTest.sol"; import {FHE} from "@fhevm/solidity/lib/FHE.sol"; import "encrypted-types/EncryptedTypes.sol"; contract MyTest is FhevmTest { MyContract myContract; function setUp() public override { super.setUp(); // deploy FHEVM host contracts myContract = new MyContract(); } }⚠️注意:被测合约必须继承一个 Zama 配置(例如
ZamaEthereumConfig),这样FHE.*调用才会路由到setUp()部署的 FHEVM host contracts 上。
关于这一点可以从仓库源码中得到印证:ZamaConfig.sol 中的ZamaEthereumConfig在构造时调用FHE.setCoprocessor(ZamaConfig.getEthereumCoprocessorConfig()),将 ACL、协处理器、KMSVerifier 的地址注入 FHE 库。该库按block.chainid路由配置:Ethereum 主网(1)、Polygon(137)、Sepolia(11155111)、Polygon Amoy(80002)以及本地 Hardhat/Anvil(31337),本地链的规范地址硬编码在_getLocalConfig()中。这正是super.setUp()能按确定性地址部署、且FHE.fromExternal能正确路由的前提。
setUp() 注入的状态变量
根据 forge-fhevm API 参考,super.setUp()会为测试合约注入以下基础设施:
| 变量 | 类型 | 作用 |
|---|---|---|
_executor | FHEVMExecutor | 处理 FHE 运算并发出驱动明文跟踪的事件 |
_acl | ACL | 按句柄的访问控制(transient 与 persistent) |
_inputVerifier | InputVerifier | 验证 EIP-712 输入证明(1 个 mock 签名者) |
_kmsVerifier | KMSVerifier | 验证 EIP-712 解密证明(1 个 mock 签名者) |
MOCK_INPUT_SIGNER | address | 模拟输入签名者地址 |
MOCK_KMS_SIGNER | address | 模拟 KMS 签名者地址 |
💡 这些 mock 签名者密钥是 Zama 特有值,硬编码在
forge-fhevm/src/FhevmTest.sol中,并非Foundry 标准的测试私钥。它们的存在仅仅是为了让测试中的 EIP-712 证明保持确定性。
加密输入:encrypt* 助手
任何调用FHE.fromExternal的合约,都需要调用方提供一个(handle, proof)对。FhevmTest提供了一组encrypt*助手来构建这个二元组。
两步/三步重载
每个助手都有两个重载:
- 两参数重载:使用
address(this)作为隐式用户(即测试合约自身):
(externalEuint64 amount, bytes memory proof) = encryptUint64(100, address(myContract));- 三参数重载:将证明绑定到指定用户:
address alice = address(0xA11CE); (externalEuint64 amount, bytes memory proof) = encryptUint64(100, alice, address(myContract));调用被测合约
拿到(handle, proof)后,用 Foundry 的vm.prank模拟用户身份发起调用:
vm.prank(alice); myContract.deposit(amount, proof);支持的加密助手一览
| 函数 | 值类型 | 返回句柄 |
|---|---|---|
encryptBool | bool | externalEbool |
encryptUint8 | uint8 | externalEuint8 |
encryptUint16 | uint16 | externalEuint16 |
encryptUint32 | uint32 | externalEuint32 |
encryptUint64 | uint64 | externalEuint64 |
encryptUint128 | uint128 | externalEuint128 |
encryptUint256 | uint256 | externalEuint256 |
encryptAddress | address | externalEaddress |
💡 每次调用
encrypt*都会递增一个内部 nonce,因此对同一个值加密两次会产生不同的句柄——这也保证了测试不会因句柄复用而出现意外碰撞。
仓库中的加密合约示例可以印证这套流程的实际用法:在 fhe-counter.md 示例 的FHECounter.increment中,合约通过FHE.fromExternal(inputEuint32, inputProof)验证并导入外部密文,随后执行FHE.add,并通过FHE.allowThis(_count)与FHE.allow(_count, msg.sender)为句柄授予 ACL 权限——这正是下文userDecrypt模式所依赖的持久化 ACL 权限来源。
解密结果:三种解密模式
forge-fhevm提供三种解密模式,分别对应生产环境的不同解密流程。选择哪一种,取决于被测合约的调用模式。
decrypt(handle)—— 底层查找
直接返回句柄对应的明文,不做任何 ACL 或证明检查。适合单元断言场景,例如检查合约内部状态:
euint64 balance = myContract.balanceHandle(alice); assertEq(decrypt(balance), 100);decrypt()为每种加密类型提供了类型化重载,返回对应的 Solidity 原始类型:
bool a = decrypt(myEbool); uint8 b = decrypt(myEuint8); uint64 c = decrypt(myEuint64); address d = decrypt(myEaddress);底层签名是decrypt(bytes32 handle) returns (uint256),各类型重载只是将其转换为匹配的 Solidity 原始类型(bool、uint8……uint256、address)。
publicDecrypt(handles)—— KMS 签名的公开解密
当被测合约在链上通过FHE.checkSignatures()验证解密证明时使用。它返回明文数组与一个 KMS 签名证明:
bytes32[] memory handles = new bytes32[](1); handles[0] = euint64.unwrap(balance); (uint256[] memory cleartexts, bytes memory proof) = publicDecrypt(handles); FHE.checkSignatures(handles, abi.encode(cleartexts), proof); assertEq(cleartexts[0], 100);⚠️ 如果被测合约未对该句柄调用过
FHE.makePubliclyDecryptable(),publicDecrypt()将回滚并抛出HandleNotAllowedForPublicDecryption错误。
userDecrypt(handle, user, contract, signature)—— 面向用户的解密流程
完整模拟生产环境的用户解密:包含持久化 ACL 检查与 EIP-712 签名验证:
uint256 constant ALICE_PK = 0xA11CE; address alice = vm.addr(ALICE_PK); // (通过业务逻辑 mint 或 transfer 将 ACL 授予 alice) bytes memory sig = signUserDecrypt(ALICE_PK, address(myContract)); uint256 cleartext = userDecrypt( euint64.unwrap(myContract.balanceHandle(alice)), alice, address(myContract), sig ); assertEq(cleartext, 100);signUserDecrypt是 EIP-712 用户解密签名助手,其完整签名族包括:
// 单合约版本 function signUserDecrypt(uint256 userPk, address contractAddress) view returns (bytes memory signature); // 多合约 + 有效期版本 function signUserDecrypt( uint256 userPk, address[] memory contractAddresses, uint256 startTimestamp, uint256 durationDays ) view returns (bytes memory signature);userDecrypt流程中可能触发的错误及原因:
| 错误 | 原因 |
|---|---|
UserAddressEqualsContractAddress | userAddress == contractAddress |
UserNotAuthorizedForDecrypt | 用户缺少持久化ACL 权限 |
ContractNotAuthorizedForDecrypt | 合约缺少持久化ACL 权限 |
InvalidUserDecryptSignature | 签名无法还原出userAddress |
💡 ACL 权限由被测合约在其业务逻辑中授予——例如在 token 的
mint中调用FHE.allow(balance, owner)。测试中无需手动授权,这正是 FHECounter.sol 同目录的docs/examples/fhe-counter.md中increment末尾FHE.allow(_count, msg.sender)的作用:业务逻辑完成后,调用方天然具备对结果的解密权限。
附加的证明助手
FhevmTest还提供两个底层证明构造助手,适用于回调风格(callback-style)的流程:
// KMS 签名的解密证明(无 ACL 检查) function buildDecryptionProof(bytes32[] memory handles, bytes memory abiEncodedCleartexts) view returns (bytes memory proof); function buildDecryptionProof(bytes32 handle, bytes memory abiEncodedCleartext) view returns (bytes memory proof);完整计数器测试示例
下面是一个完整的、开箱即用的 FHE 计数器测试(原文档引自fhevm-foundry-template/test/FHECounter.t.sol,仓库内与之对应的合约实现在 docs/examples/fhe-counter.md 的FHECounter.sol中):
contract FHECounterTest is FhevmTest { FHECounter counter; uint256 internal constant ALICE_PK = 0xA11CE; address alice; function setUp() public override { super.setUp(); counter = new FHECounter(); alice = vm.addr(ALICE_PK); } function test_incrementTheCounterByOne() public { (externalEuint32 encOne, bytes memory proof) = encryptUint32(1, alice, address(counter)); vm.prank(alice); counter.increment(encOne, proof); bytes memory sig = signUserDecrypt(ALICE_PK, address(counter)); uint256 clear = userDecrypt(euint32.unwrap(counter.getCount()), alice, address(counter), sig); assertEq(clear, 1); } }这个测试完整覆盖了 FHEVM 合约的典型生命周期:
- 加密:
encryptUint32(1, alice, address(counter))为用户 alice 生成指向计数器合约的加密输入; - 执行:
vm.prank(alice)模拟 alice 调用counter.increment(encOne, proof),链上完成FHE.fromExternal输入验证与FHE.add密文运算; - 解密:
signUserDecrypt生成 EIP-712 签名,userDecrypt走完整的 ACL + 签名验证流程取回明文; - 断言:
assertEq(clear, 1)验证结果。
运行测试
在项目根目录执行:
forge test -vvv只运行单个测试:
forge test --match-test test_incrementTheCounterByOne -vvv # single test-vvv级别会输出完整的事件与 trace,便于排查FHE.fromExternal验证失败或 ACL 报错等链上细节。
项目环境配置要点
为了让上述测试代码可以运行,需要正确的 Foundry 工程配置(详见 Setup Foundry):
foundry.toml关键项:forge-fhevm面向 Cancun EVM 与较新的 Solidity 编译器,evm_version = "cancun"是必要配置;依赖通过 Soldeer 管理,安装命令为forge soldeer install。
remappings.txt依赖映射(版本占位符按 Soldeer 实际写入的目录替换):
@fhevm/host-contracts/=dependencies/forge-fhevm-<rev>/src/fhevm-host/ @fhevm/solidity/=dependencies/@fhevm-solidity-<version>/ encrypted-types/=dependencies/@encrypted-types-<version>/ forge-fhevm/=dependencies/forge-fhevm-<rev>/src/ forge-std/=dependencies/forge-std-<version>/src验证安装:可以写一个最小测试确认 host contracts 部署成功:
// test/Setup.t.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; import {FhevmTest} from "forge-fhevm/FhevmTest.sol"; contract SetupTest is FhevmTest { function test_setupDeploys() public view { // setUp() deploys all FHEVM host contracts at deterministic addresses assertTrue(address(_executor) != address(0)); assertTrue(address(_acl) != address(0)); } }forge test --match-test test_setupDeploys -vv测试与部署的衔接
测试通过后,合约可以沿两条路径部署(详见 Deploy FHEVM contracts with Foundry):
- Sepolia 测试网:FHEVM 协议栈已部署在规范地址上,合约通过继承的
ZamaEthereumConfig自动获取 ACL、协处理器与 KMSVerifier 地址,只需用forge script --rpc-url <sepolia> --broadcast --verify广播部署脚本; - 本地 Anvil 节点:先用 forge-fhevm 的
deploy-local.sh将 host contracts 以setCode/setStorageAt方式物化到本地节点的规范地址,再部署自己的合约。
这与测试环境的行为保持一致:测试与本地部署共享同一套ZamaConfig._getLocalConfig()规范地址(chainId 31337),因此"测试通过"对本地部署行为具有直接参考价值。
小结
forge-fhevm让 FHEVM 合约测试回归到与普通合约相同的开发体验:FhevmTest基类 +super.setUp()完成全部链上基础设施搭建,encrypt*系列助手解决密文输入的构造与证明生成,三种解密模式分别对应用户解密、公开解密与底层断言三种生产场景。配合本仓库的 FHE 库源码、示例合约 以及 forge-fhevm API 参考,你可以在几分钟内为任意 FHEVM 合约搭建出覆盖"加密 → 执行 → 解密 → 断言"完整闭环的测试套件。
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考