fhEVM Foundry 测试核心 API 速查:FhevmTest 基座合约的加密、解密与证明辅助函数全解析
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
本文是 fhEVM(Fully Homomorphic Encryption EVM)项目中 forge-fhevm 测试库FhevmTest基座合约的 API 速查指南。你将在编写基于 Foundry 的 fhEVM 合约测试时,用FhevmTest在本地测试 EVM 中重建FHEVMExecutor、ACL、InputVerifier、KMSVerifier等主机合约栈,并通过encrypt*/decrypt/publicDecrypt/userDecrypt等辅助函数完成"加密输入 → 链上执行 → 解密断言"的完整测试闭环。读完本文,你将掌握FhevmTest的全部公开 API、常量语义及其背后的源码级原理,可直接上手编写与生产路径一致的 fhEVM 合约测试。
本文为
docs/solidity-guides/foundry/api.md的展开版,补齐了源码级实现证据与完整用法说明。关于项目初始化与测试编写流程,可结合阅读 Foundry 入门、Setup Foundry、编写 fhEVM 测试 与 Foundry 部署指南。
1. FhevmTest 是什么:Foundry 环境中的 fhEVM 主机合约栈
FhevmTest是 forge-fhevm 提供的、面向 fhEVM 机密智能合约的 Foundry 原生测试基座合约。它的核心设计目标,是让被测合约在 Foundry 测试 EVM 中运行与生产环境完全一致的链上代码路径——输入证明验证(EIP-712)、ACL 权限强制、handle(句柄)生命周期管理都按主网真实逻辑执行;唯一的例外是 FHE 协处理器计算本身被模拟,明文值在本地被追踪,因此你可以直接对解密结果做assertEq断言。
从仓库源码可以印证这套"测试即生产"的思路:本仓库的 host-contracts/fhevm-foundry/HostContractsDeployerTestUtils.sol 就是一个在 Foundry 内重建主机合约栈的测试装配器——它把ACL、FHEVMExecutor、KMSVerifier、InputVerifier、HCULimit、PauserSet、ProtocolConfig、KMSGeneration等通过deployCodeTo部署到各自规范地址(canonical address)上的空代理中,再执行带 initializer payload 的特权升级调用,从而让跨合约权限检查(ACLOwnable、slot 读取等)与链上行为完全一致。FhevmTest.setUp()在语义上做的正是同类事情,只是封装成了开箱即用的基座合约。
FhevmTest开箱提供的能力包括:
- 覆盖所有 FHE 类型的加密辅助函数(
encryptBool、encryptUint8…encryptUint256、encryptAddress); - 三种解密模式:底层
decrypt()、publicDecrypt()、userDecrypt(); - EIP-712 证明辅助函数(
signUserDecrypt、buildDecryptionProof); - 一个在
setUp()中部署好全部基础设施的FhevmTest基座合约。
与主网唯一的偏差,是输入签名者和 KMS 签名者使用 mock 私钥,从而保证测试中 EIP-712 证明的确定性生成。
2. 导入 FhevmTest
在测试合约中通过 Soldeer 安装的forge-fhevm依赖路径导入:
import {FhevmTest} from "forge-fhevm/FhevmTest.sol";导入后,测试合约继承FhevmTest,并在setUp()中调用super.setUp()完成主机合约的部署:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.27; import {FhevmTest} from "forge-fhevm/FhevmTest.sol"; contract MyTest is FhevmTest { function setUp() public override { super.setUp(); // 在规范地址上部署全部 fhEVM 主机合约 // 然后实例化被测合约…… } }需要说明的是,被测合约必须继承 Zama 配置(例如ZamaEthereumConfig),使FHE.*调用路由到setUp()部署的 fhEVM 主机合约,否则加密类型与执行器无法正确接线。
3. setUp() 部署的状态变量
setUp()完成后,FhevmTest会暴露以下状态变量,供测试直接使用:
| 变量 | 类型 | 角色 |
|---|---|---|
_executor | FHEVMExecutor | 处理 FHE 操作,并发出驱动明文追踪的事件 |
_acl | ACL | 按 handle 进行访问控制(瞬态与持久化权限) |
_inputVerifier | InputVerifier | 验证 EIP-712 输入证明(1 个 mock 签名者) |
_kmsVerifier | KMSVerifier | 验证 EIP-712 解密证明(1 个 mock 签名者) |
MOCK_INPUT_SIGNER | address | mock 输入签名者地址 |
MOCK_KMS_SIGNER | address | mock KMS 签名者地址 |
这些角色与生产部署一一对应:FHEVMExecutor是 FHE 运算的执行入口,ACL维护每个密文 handle 的访问权限,InputVerifier校验FHE.fromExternal的输入证明,KMSVerifier校验解密证明的 KMS 门限签名。对照本仓库的部署装配器 HostContractsDeployerTestUtils.sol,_deployFullHostStack会把ACL、PauserSet、FHEVMExecutor、HCULimit、ProtocolConfig、KMSGeneration、KMSVerifier、InputVerifier全部拉起,并通过require断言执行器与 ACL、HCU 的地址接线、KMS 门限与输入签名者门限的配置均正确——这正是setUp()背后"生产同构"的工程保证。
4. 加密辅助函数:为被测合约构造 (handle, proof)
每个加密辅助函数都有两种重载:
- 两参数重载:隐式用户为
address(this)(即测试合约自身); - 三参数重载:显式指定用户。
function encryptBool(bool value, address target) returns (externalEbool, bytes memory); function encryptBool(bool value, address user, address target) returns (externalEbool, bytes memory); function encryptUint8(uint8 value, address target) returns (externalEuint8, bytes memory); function encryptUint8(uint8 value, address user, address target) returns (externalEuint8, bytes memory); // 相同形态:encryptUint16, encryptUint32, encryptUint64, // encryptUint128, encryptUint256, encryptAddress其中target是最终调用FHE.fromExternal的合约地址,user是证明绑定的用户。完整的支持矩阵(源自write_test.md的表格)如下:
| 函数 | 值类型 | 返回的句柄 |
|---|---|---|
encryptBool | bool | externalEbool |
encryptUint8 | uint8 | externalEuint8 |
encryptUint16 | uint16 | externalEuint16 |
encryptUint32 | uint32 | externalEuint32 |
encryptUint64 | uint64 | externalEuint64 |
encryptUint128 | uint128 | externalEuint128 |
encryptUint256 | uint256 | externalEuint256 |
encryptAddress | address | externalEaddress |
典型用法是先加密得到(handle, proof)对,再以vm.prank模拟用户调用被测合约:
// 隐式用户(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)); vm.prank(alice); myContract.deposit(amount, proof);实现细节提示:每次调用encrypt*都会使内部 nonce 递增,因此对同一值加密两次会得到不同的 handle。这一设计保证了测试中密文句柄的不可预测性,与生产环境每个输入产生唯一句柄的语义一致。
5. 解密辅助函数:三种与生产流程对齐的解密模式
FhevmTest提供三种解密模式,分别对应生产环境中的不同解密路径,按被测合约的交互模式选用。
5.1decrypt(handle)—— 底层直查
不做 ACL 检查、不做证明校验,直接返回 handle 对应的明文uint256,最适合单元断言:
function decrypt(bytes32 handle) returns (uint256);同时提供针对每种加密类型的强类型重载,返回值是匹配的 Solidity 原生类型:
function decrypt(ebool value) returns (bool); function decrypt(euint8 value) returns (uint8); function decrypt(euint16 value) returns (uint16); function decrypt(euint32 value) returns (uint32); function decrypt(euint64 value) returns (uint64); function decrypt(euint128 value) returns (uint128); function decrypt(euint256 value) returns (uint256); function decrypt(eaddress value) returns (address);用法示例:
euint64 balance = myContract.balanceHandle(alice); assertEq(decrypt(balance), 100); bool a = decrypt(myEbool); uint8 b = decrypt(myEuint8); uint64 c = decrypt(myEuint64); address d = decrypt(myEaddress);5.2publicDecrypt(handles)—— KMS 签名的公开解密
适用于被测合约通过FHE.checkSignatures()在链上验证解密证明的回调式流程。返回明文数组与 KMS 签名的证明:
function publicDecrypt(bytes32[] memory handles) returns (uint256[] memory cleartexts, bytes memory proof);用法示例:
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);注意:若被测合约没有对该 handle 调用FHE.makePubliclyDecryptable(),publicDecrypt()会以HandleNotAllowedForPublicDecryption回滚。
从源码看,FHE.checkSignatures所校验的正是 KMS 对PublicDecryptVerification(bytes32[] ctHandles,bytes decryptedResult,bytes extraData)类型哈希的 EIP-712 门限签名——该类型哈希与DECRYPTION_RESULT_TYPEHASH定义在 library-solidity/lib/FHE.sol 中,底层由KMSVerifier.verifyDecryptionEIP712KMSSignatures(见 FHE.sol)执行多签名验证与门限判定。
5.3userDecrypt(handle, user, contract, signature)—— 面向用户的完整流程
实现带持久化 ACL 检查与 EIP-712 签名验证的完整用户解密流程:
function userDecrypt( bytes32 handle, address userAddress, address contractAddress, bytes memory userSignature ) returns (uint256);用法示例(配合signUserDecrypt生成用户签名):
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);userDecrypt可能抛出的错误及其原因:
| 错误 | 原因 |
|---|---|
UserAddressEqualsContractAddress | userAddress == contractAddress |
UserNotAuthorizedForDecrypt | 用户缺少持久化ACL 权限 |
ContractNotAuthorizedForDecrypt | 合约缺少持久化ACL 权限 |
InvalidUserDecryptSignature | 签名无法恢复出userAddress |
关键点:ACL 权限是由被测合约在业务逻辑中授予的(例如代币mint时调用FHE.allow(balance, owner)),测试中无需手动授权。这保证了userDecrypt测试的是真实业务权限流,而不是绕过 ACL 的桩逻辑。
6. 证明辅助函数:构建 EIP-712 签名与解密证明
6.1buildDecryptionProof—— 构建 KMS 签名的解密证明
用于回调式(callback-style)流程,不做 ACL 检查:
// 批量版本:一次构建多个 handle 的 KMS 签名解密证明 function buildDecryptionProof(bytes32[] memory handles, bytes memory abiEncodedCleartexts) view returns (bytes memory proof); // 单 handle 版本 function buildDecryptionProof(bytes32 handle, bytes memory abiEncodedCleartext) view returns (bytes memory proof);该证明由 mock KMS 签名者按MOCK_KMS_SIGNER_PK生成,可在测试中直接作为FHE.checkSignatures的第三个参数,从而在不依赖真实 KMS 网络的前提下验证被测合约的链上解密校验逻辑。
6.2signUserDecrypt—— 生成 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);其中userPk是测试中通过vm.addr(userPk)派生的用户私钥,与userDecrypt中的userAddress对应。默认有效期由常量DEFAULT_USER_DECRYPT_DURATION_DAYS(值为1)控制。
7. 常量一览
| 常量 | 值 | 用途 |
|---|---|---|
MOCK_INPUT_SIGNER_PK | 硬编码 mock 密钥——见FhevmTest.sol | 签署输入证明(确定性、mock 签名者) |
MOCK_KMS_SIGNER_PK | 硬编码 mock 密钥——见FhevmTest.sol | 签署 KMS 解密证明(确定性、mock 签名者) |
EMPTY_EXTRA_DATA | hex"00" | 附加到 EIP-712 证明上的默认 extra data |
DEFAULT_USER_DECRYPT_DURATION_DAYS | 1 | 用户解密签名的默认有效期(天) |
重要提示:这两个 mock 签名者私钥是 Zama 特有的、固化在
forge-fhevm/src/FhevmTest.sol中的值,并非Foundry 的标准测试私钥。它们存在的唯一目的,是让测试中的 EIP-712 证明具有确定性。任何依赖这些密钥的安全性假设,都只适用于本地测试环境。
8. 从速查到实战:API 在完整测试中的落地
FhevmTest的 API 组合起来,可以覆盖 fhEVM 合约测试的完整生命周期。一个端到端示例(计数器合约测试,对应文档write_test.md中的完整示例)如下:
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); } }运行测试:
forge test -vvv forge test --match-test test_incrementTheCounterByOne -vvv # 只跑单个测试对应地,本仓库中的示例合约 library-solidity/examples/Counter.sol 展示了明态计数器(uint32 value+increment()/currentValue())的形态;而 fhEVM 场景下,value会被替换为euint32加密句柄,increment接收externalEuint32与证明并调用FHE.asEuint32,读取则通过解密辅助函数完成——FhevmTest的整套 API 正是为验证这类"加密输入 → 链上密态运算 → 授权解密"流程而设计的。
9. 进一步阅读
- Foundry 测试库总览:forge-fhevm 的设计理念与目录导航;
- Setup Foundry:从模板克隆项目、Soldeer 安装依赖、
foundry.toml/remappings.txt配置; - 编写 fhEVM 测试:三种解密模式的完整用例与错误码对照表;
- Foundry 部署指南:将合约部署到本地 Anvil 或 Sepolia;
- FHE 交互库源码:
FHE.fromExternal、FHE.allow、FHE.checkSignatures、FHE.makePubliclyDecryptable等链上函数的真实实现; - 主机合约测试装配器:
FhevmTest.setUp()同构思想在仓库内的源码级印证。
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考