fhEVM 端到端测试套件实战指南:SDK 源切换、统一用户解密、算子边界与 Smoke 冒烟运行
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
导读
本文以test-suite/e2e为核心,完整讲解 fhEVM 全栈框架中端到端(E2E)测试工程的设计与用法,覆盖四大主题:@fhevm/sdk依赖源(本地构建 vs npm registry)的切换机制、基于统一 EIP-712 信封(eip712-unified-user-decrypt-v1)的用户解密测试套件、算术/移位/旋转/类型转换算子的边界用例,以及面向 Sepolia/Mainnet/Devnet 的生产级冒烟运行器(smoke runner)。读完本文,你将掌握如何在真实网络上配置环境变量、控制签名者故障切换与 gas 策略,并理解 E2E 测试中"哪些拒绝发生在 relayer 层、哪些只发生在 KMS Connector 层"的断言模型。
一、测试套件全景:目录结构与角色定位
test-suite/e2e是 fhEVM 仓库(根目录 README.md)中基于 Hardhat 的端到端测试工程,其 package.json 声明了@fhevm/e2e-suite这一 npm 包名,依赖hardhat@^2.23.0、ethers@^6.15.0、@openzeppelin/contracts@^5.3.0、@safe-global/safe-contracts等。整个工程围绕四类测试资产组织:
| 资产类别 | 关键路径 | 职责 |
|---|---|---|
| 合约源码 | contracts/ | 冒烟合约SmokeTestInput、TestInput,算子套件FHEVMOperatorEdgeCaseTestSuite、FHEVMManualTestSuite,ERC-1271 钱包ERC1271ApproveHashWallet/ERC1271OwnerWallet/ERC1271RejectWallet等 |
| 测试用例 | test/ | 解密、算子、桥接、多链、暂停协议、共识等待等 100+ 个 TS 用例 |
| 运行脚本 | scripts/ | install-sdk.sh(SDK 安装切换)、smoke-inputflow.ts(冒烟运行器)、smoke-reporting.ts(失败分类与上报)、gen_handles.ts |
| 运行入口 | run-tests.sh、hardhat.config.ts | 测试过滤/网络选择与 Hardhat 网络定义 |
hardhat.config.ts 定义了丰富的网络拓扑:staging(默认)、zwsDev、sepolia、mainnet、polygon、polygonAmoy,以及本地/容器化协处理器链localCoprocessor、composeCoprocessorL1/L2等,统一使用 HD 钱包(120 个账户,由MNEMONIC派生,默认派生路径m/44'/60'/0'/0),并为 staging/zwsDev 场景默认回落到http://localhost:8545。
二、@fhevm/sdk依赖源切换:本地构建与 registry 版本
2.1 为什么需要单独安装 SDK
test-suite/e2e不在package.json中声明@fhevm/sdk依赖——它由 scripts/install-sdk.sh 在安装时原地注入,既可以从本地源码构建打包,也可以从 npm registry 安装固定版本,全程不触碰package.json/package-lock.json。这种设计让测试套件既能验证仓库内sdk/js-sdk的最新改动,又能对照某个已发布版本做回归。
2.2 使用本地源码构建(默认路径)
在 Docker 构建与默认场景下,SDK 从仓库内的sdk/js-sdk源码构建并打包安装:
cd test-suite/e2e npm run sdk:localnpm run sdk:local实际执行./scripts/install-sdk.sh local。从 install-sdk.sh 的源码可以看到其关键行为:
- 调用
sdk/js-sdk/test/scripts/rebuild_sdk_and_pack.sh --build-profile=$BUILD_PROFILE完成构建与打包,产物为sdk/js-sdk/test/manual-pack/fhevm-sdk-*.tgz; - 在
test-suite/e2e目录执行npm install --no-save --no-workspaces --legacy-peer-deps安装该 tarball; - 脚本对
--no-workspaces与--legacy-peer-deps有专门注释说明:前者避免 npm 把@fhevm/sdk提升(hoist)到仓库根node_modules导致 ethers 无法解析;后者因为本次安装是从零解析依赖树(不同于根目录信任 lockfile 的npm ci),而@safe-global/safe-contracts的 peer 依赖ethers@5.4.0与项目其余部分的ethers@^6.15.0冲突,需要--legacy-peer-deps放行; - 安装完成后脚本会恢复
@fhevm/solidity的 workspace 链接:--no-workspaces安装会把@fhevm/solidity从 workspace link(指向library-solidity)替换成 registry 的真实副本,导致 E2E 悄悄编译到可能过期的 Solidity 库,因此脚本删除后重新软链回$ROOT_DIR/library-solidity。
迭代提示:本地安装是一次性打包(one-off pack),并非实时软链接——每次修改sdk/js-sdk源码后都必须重新运行npm run sdk:local。
2.3 加速开发:SDK_BUILD_PROFILE=dev
默认构建 profile 为prod(见 install-sdk.sh 中BUILD_PROFILE="${SDK_BUILD_PROFILE:-prod}")。在开发迭代期间,可以先设置SDK_BUILD_PROFILE=dev再执行npm run sdk:local,获得更快、未压缩(unminified)的构建产物,便于断点调试:
cd test-suite/e2e SDK_BUILD_PROFILE=dev npm run sdk:local2.4 安装特定 registry 版本
npm run sdk:registry -- <version>对应install-sdk.sh registry <version>。registry 模式必须显式传版本号,没有默认版本可回退(因为 package.json 故意不固定@fhevm/sdk):
cd test-suite/e2e npm run sdk:registry -- 0.13.2两种模式共用同一套npm install --no-save --no-workspaces --legacy-peer-deps安装策略,并同样在结尾恢复@fhevm/solidityworkspace 链接。install-sdk.sh 的文件头注释明确了这套机制复用了sdk/js-sdk/test/browser-next下refresh-sdk.sh的npm install --no-save技术。
三、统一用户解密(Unified User-Decryption)套件
3.1 三个测试目录与覆盖范围
README 定义的统一解密 E2E 覆盖分为三个目录,围绕 ERC-1271 智能账户签名验证与统一的 EIP-712 用户解密请求展开:
- test/erc1271UserDecryption/ —— 智能账户签名模式与全部 ERC-1271 拒绝路径。目录内含
safe.ts(真实 Safe 多签签名拼接)、erc1271SdkClientGap.ts,配套合约见 contracts/erc1271/; - test/unifiedUserDecryption/ ——
allowedContracts模式、有效期窗口(validity window)、直接+委托混合批次(mixed direct+delegated batches)、extraData版本矩阵; - test/decryptionSignatureInvalidation/ —— 链上签名失效及其端到端影响,包括多签轮换(multisig-rotation)场景。
3.2 为什么需要自研客户端
这些套件直接向 relayer 的/v3/user-decrypt端点 POST 统一的eip712-unified-user-decrypt-v1信封,客户端封装在 test/sdk/unified/unifiedUserDecrypt.ts。该文件头注释解释得很清楚:公共@fhevm/sdk在 protocol >= 0.14 时也构造同一信封,但总是以连接的 signer 身份签名,且不暴露这些套件必须控制的字段——与 ECDSA 签名者不同的userAddress(智能账户)、空签名(approveHash 流程)、自定义extraData版本、自定义startTimestamp、以及刻意构造的畸形形状(用于负向用例)。
3.3 信封结构与 EIP-712 类型
UNIFIED_ATTESTATION_TYPE = 'eip712-unified-user-decrypt-v1'是 relayer/v3/user-decrypt端点唯一接受的 attestation 类型。EIP-712 类型列表UNIFIED_USER_DECRYPT_TYPES定义如下,字段顺序具有权威性——它决定 EIP-712 类型哈希,必须与 Solidity 结构体、KMS Connector、relayer、js-sdk 的kmsUserDecryptEip712V2Types完全一致,注释明确警告"不要重排":
UserDecryptRequestVerification: [ { name: 'userAddress', type: 'address' }, { name: 'publicKey', type: 'bytes' }, { name: 'allowedContracts',type: 'address[]' }, { name: 'startTimestamp', type: 'uint256' }, { name: 'durationSeconds', type: 'uint256' }, { name: 'extraData', type: 'bytes' }, ]EIP-712 domain 的name/version取自 GatewayDecryption合约:DOMAIN_NAME = 'Decryption'、DOMAIN_VERSION = '1'。默认extraData为0x00(版本字节 0,无上下文 ID)。值得注意的一个实现细节是 chainIdFromHandle:EIP-712 domain 的chainId不是从配置读取,而是从密文 handle 本身推导——FHEVM handle 以大端字节序在第 [22, 30) 字节编码创建时的链 ID,与 relayer 签名预检读取的切片一致,从而保证 digest 与 relayer 重算结果匹配。
3.4 四种签名模式(SignMode)
统一解密请求支持四种签名生成方式:
| 模式 | 语义 | 适用场景 |
|---|---|---|
eoa | signer直接签名,且signer.address必须等于userAddress(EOA 快速路径);签名前会显式校验二者一致,否则报错提示改用erc1271 | 常规 EOA 用户解密 |
erc1271 | 由ownerSigner(某位 owner 的私钥)签名,userAddress为智能钱包地址,KMS/relayer 通过钱包的isValidSignature验证 | 智能账户(Smart Account) |
empty | 不提供签名(0x),对应 Safe 的approveHash/signedMessages流程 | Safe 预先批准 |
raw | 直接透传预先构造好的签名 blob——包括 Safe 多签拼接(见test/erc1271UserDecryption/safe.ts的buildSafeMultisigSignature)以及故意构造的畸形 blob(负向用例) | 多签与畸形输入 |
3.5 断言模型:拒绝发生在哪一层
该 helper 的注释文档化了完整的断言模型,这是理解套件写法的关键。POST /v3/user-decrypt的响应语义为:202(携带jobId)= 接受;其他状态码 = 拒绝。拒绝可分为三个层次:
- Relayer 同步预检(签名验证):共享的
verify_signature先做ecrecover,失败再回退 ERC-1271。确定性的坏签名得到POST 400 invalid_signature;合法签名得到POST 202 queued。isSignatureRejection通过匹配{field: "signature", issue: "Signature is invalid"}将"签名语义拒绝"与"信封签名字段校验失败(畸形 hex)"区分开——后者共享相同 label/field 但 issue 文本不同。 - Relayer 每任务 host-ACL 检查:逐 handle 的所有权/委托 ACL 失败会以终态
failed呈现,error.label == "not_allowed_on_host_acl"——用expectRelayerAclRejection断言,钉死失败原因,防止测试在意外失败上"蒙混过关"。 - 仅 KMS Connector 强制执行的检查:
allowedContracts语义、签名失效、extraData上下文/epoch 校验。这类拒绝不会向 relayer 返回可见响应——任务在整个观察窗口内保持queued。用expectStuckAtKms断言状态恰好为pending,从而把拒绝模式钉死在"KMS 层拒绝",任何意外失败(坏签名→400、ACL 失败→failed)都会使该断言失败。KMS-Connector 拒绝永不触达 relayer,任务会在 relayer 默认的user_decrypt_timeout(30 分钟)才被收割,因此套件的观察窗口选择为"短于任何 relayer 侧超时、长于真实成功所需时间",窗口结束时仍pending即为真实拒绝。
此外还有expectGatewayRevert(断言 Gateway 链上模拟失败导致任务终态failed)、requestUnifiedUserDecrypt(构造+签名+提交+可选轮询的端到端便捷入口,其中对"202 却无 jobId"的情况快速失败以避免负向用例空转通过)。正向用例凡是 SDK 可表达的(EOA 或委托 handle),会额外通过公共 SDK 解密同一 handle 并断言已知明文;而钱包所有 handle(userAddress为合约)无法走公共 SDK 解密,其正向用例只断言succeeded。
3.6 运行方式
可通过 fhevm-cli profiles 运行(erc1271-user-decryption、unified-user-decryption、decryption-signature-invalidation,均属于standard测试车道,详见 test-suite/fhevm/README.md),也可直接用 Hardhat 过滤运行:
npx hardhat test --grep "<describe title>" --network staging如果 relayer 前面挂了认证网关,需要设置ZAMA_FHEVM_API_KEY(以x-api-key头发送;与 js-sdkApiKeyHeader认证模式使用的默认 header 一致)。
四、算子边界用例套件(Operator Edge Cases)
4.1 覆盖范围
test/fhevmOperations/下的operatorEdgeCases*.ts系列覆盖算术、移位、旋转和类型转换算子的极限情况:
- 过度移位(overshift,移位量 >= 位宽);
- 除法/取模边界与
DivisionByZero()revert; - 上溢/下溢回绕(over/underflow wrapping);
- 窄化类型转换截断(narrowing-cast truncation)。
4.2 语义参考与 tfhe-rs 版本联动
期望值定义在 test/fhevmOperations/shiftSemantics.ts,它以OVERSHIFT_RETURNS_ZERO = false为开关实现"参考语义"。从源码可见:
- 当
OVERSHIFT_RETURNS_ZERO = false(当前值)时,采用传统语义:过大的移位量被截断为amount % bits后再移位(expectedShl/expectedShr); - 旋转算子
expectedRotl/expectedRotr恒按amount % bits计算; - 注释明确:tfhe-rs >= 1.7.0 时过度移位返回 0 而非截断移位量,因此升级引擎版本时必须同步把
OVERSHIFT_RETURNS_ZERO翻转为true,二者必须同一改动提交,否则边界用例会与引擎行为不一致。
4.3 运行命令
./fhevm-cli test operators --grep "edge cases" --verbosefhevm-cli位于 test-suite/fhevm,其test heavy车道即算子覆盖车道,operators测试默认自动启用--parallel并行。
五、Smoke 冒烟运行器(inputFlow)
5.1 定位
scripts/smoke-inputflow.ts 以 Hardhat 为运行时、配合加固的事务处理逻辑,运行单条链上冒烟流程:加密输入(encrypt uint64=7)→ 链上调用add42ToInput64→ 用户解密 + 公开解密,断言结果均为49。流程细节可从源码确认:先通过createInstance()初始化 fhEVM 实例并加密值7n,以instance.encryptUint64产出 handle 与 inputProof,再调用合约的add42ToInput64(handle, inputProof),最后依次执行userDecryptSingleHandle与publicDecrypt([handle]),分别断言49n与{handle: 49n}。
5.2 前置条件(Prereqs)
Sepolia/Mainnet:大部分配置由 SDK 自动填充(SepoliaConfig/MainnetConfig),只需提供:
RPC_URL(或SEPOLIA_ETH_RPC_URL/MAINNET_ETH_RPC_URL)MNEMONICZAMA_FHEVM_API_KEY(仅 mainnet 需要)
Devnet:使用预配置的 .env.devnet(全部地址已内置):
DOTENV_CONFIG_PATH=./.env.devnet npx hardhat run --network devnet scripts/smoke-inputflow.ts其他网络(staging、自定义):需手工设置全部变量,参考 .env.example(包含 Gateway 的CHAIN_ID_GATEWAY/DECRYPTION_ADDRESS、Host 的ACL_CONTRACT_ADDRESS/KMS_VERIFIER_CONTRACT_ADDRESS/FHEVM_EXECUTOR_CONTRACT_ADDRESS等)。
网络相关的 RPC URL 规则(与 hardhat.config.ts 的网络定义对应):
- staging / zwsDev:
RPC_URL(默认回落 localhost:8545) - sepolia:
SEPOLIA_ETH_RPC_URL(回落到RPC_URL) - mainnet:
MAINNET_ETH_RPC_URL(回落到RPC_URL) - Pod 部署:只需设置
RPC_URL,对所有网络通用
设置TEST_INPUT_CONTRACT_ADDRESS可复用已部署的合约(需要SMOKE_DEPLOY_CONTRACT=0)。Hardhat 默认从test-suite/e2e/.env加载环境变量,可用DOTENV_CONFIG_PATH覆盖;也可用 Hardhat vars 存储密钥,例如npx hardhat vars set SEPOLIA_ETH_RPC_URL(会交互式提示输入)。
5.3 签名者配置与故障切换
冒烟运行器使用由MNEMONIC派生的 HD 钱包签名者,默认使用索引0,1,2做自动故障切换:某个签名者事务卡住时切换到另一个。实现细节见 smoke-inputflow.ts:
- 启动时打印所有可用签名者的
latest/pendingnonce 与余额,余额低于0.005 ETH(LOW_BALANCE_THRESHOLD)时告警; - 选号器优先选"干净"(pending == latest)且余额充足(按当前 base fee × 约 100 万 gas 估算
minUsableBalance)的签名者;无干净签名者时,若 backlog 不超过SMOKE_MAX_BACKLOG(默认 3)且余额足以支付取消费用,则先对主签名者执行 backlog 取消(每个 pending nonce 发送一笔 0 值、gasLimit=21000的自转账); - 事务发送采用
sendWithRetries:在 base fee 基础上按feeBump逐次指数抬价,并区分 ethers v6 的CALL_EXCEPTION(已上链但 revert,终态错误不重试)、TIMEOUT(超时,抬价重试)、TRANSACTION_REPLACED(仅repriced视为成功;cancelled/replaced视为非本进程事务并告警重试),最终还会扫描所有已发送 hash 兜底查找"迟到上链"的回执; - 成功后清理:对仍有 backlog 的签名者尝试取消 pending 事务,但清理步骤失败不判定冒烟失败。
用 Foundrycast从助记词派生签名者地址以便打款:
cast wallet address --mnemonic "your mnemonic here" --mnemonic-index 0 cast wallet address --mnemonic "your mnemonic here" --mnemonic-index 1 cast wallet address --mnemonic "your mnemonic here" --mnemonic-index 25.4 冒烟专用旋钮(默认值)
| 环境变量 | 默认值 | 说明 |
|---|---|---|
SMOKE_SIGNER_INDICES | 0,1,2 | 用于故障切换的签名者索引,逗号分隔 |
SMOKE_TX_TIMEOUT_SECS | 48 | 单笔事务等待超时(对应 12 秒/块 × 4 块) |
SMOKE_TX_MAX_RETRIES | 2 | 事务最大重试次数 |
SMOKE_FEE_BUMP | 1.125^4 | 每次重试的费率抬升乘数 |
SMOKE_MAX_FEE_GWEI | 未设置(无上限) | maxFeePerGas超过此上限则快速失败 |
SMOKE_MAX_PRIORITY_FEE_GWEI | 未设置(无上限) | maxPriorityFeePerGas超过此上限则快速失败 |
SMOKE_MAX_BACKLOG | 3 | 允许自动取消的 pending 事务数上限 |
SMOKE_CANCEL_BACKLOG | 1 | 置0关闭 pending 事务自动取消 |
SMOKE_DEPLOY_CONTRACT | 1 | 置0则通过TEST_INPUT_CONTRACT_ADDRESS挂接已有合约 |
SMOKE_RUN_TESTS | 1 | 置0仅部署合约不运行测试 |
SMOKE_DECRYPT_TIMEOUT_SECS | 300 | 解密操作超时 |
BETTERSTACK_HEARTBEAT_URL | 未设置 | 成功时 ping BetterStack;失败时以错误码上报 |
gas 上限同样在源码中有据可查:部署与add42ToInput64调用前都会先estimateGas,再乘以120%作为 gasLimit 缓冲;SMOKE_GAS_ESTIMATE = 1_000_000(约 100 万 gas 覆盖部署+调用)。失败分类与上报由 scripts/smoke-reporting.ts 完成,它会输出SMOKE_FAILED class=...、SMOKE_FAILED_SUMMARY、SMOKE_FAILED_RELAYER_META(从异常中提取 relayer URL/操作/jobId/响应体)等结构化日志,并把timeout/decrypt_timeout/no_clean_signer/tx_all_attempts_failed等归类为告警提示(hint)。
5.5 运行
cd test-suite/e2e npx hardhat run --network zwsDev scripts/smoke-inputflow.ts npx hardhat run --network sepolia scripts/smoke-inputflow.ts npx hardhat run --network mainnet scripts/smoke-inputflow.ts六、快速运行任意测试:run-tests.sh 与 fhevm-cli
除冒烟脚本外,run-tests.sh 是通用测试入口,参数包括-g/--grep(测试过滤文本,默认test user input uint64)、-n/--network(默认staging)、-v/--verbose、--parallel、--no-hardhat-compile(跳过编译,因为test默认会通过 Hardhat 重新编译合约):
./run-tests.sh ./run-tests.sh -g "test user input uint64" ./run-tests.sh -n staging -g "my test" ./run-tests.sh "my test" # 位置参数同样生效 ./run-tests.sh --parallel --no-hardhat-compile -n sepolia -g "decryption"更上层的组织方式是 test-suite/fhevm 的fhevm-cliprofiles:test light为轻量冒烟车道(input-proof+erc20),test standard为默认 CI 车道(含 DB 回滚与漂移),test multi-chain-isolation为多链覆盖车道,test heavy为算子车道(详见 test-suite/fhevm/README.md)。
七、可继续深入的相关仓库资源
- test-suite/e2e/README.md:本测试工程的官方说明;
- test-suite/e2e/hardhat.config.ts:全部网络、链 ID、HD 账户与 gas 报告配置;
- test-suite/e2e/scripts/smoke-inputflow.ts:冒烟运行器完整实现(签名者选择、fee bump、backlog 取消);
- test-suite/e2e/test/sdk/unified/unifiedUserDecrypt.ts:统一解密客户端与断言辅助函数;
- test-suite/e2e/test/fhevmOperations/shiftSemantics.ts:移位/旋转语义参考与 tfhe-rs 版本联动开关;
- test-suite/fhevm/README.md:fhevm-cli 车道与 profiles 定义;
- sdk/js-sdk:被
install-sdk.sh local模式构建打包的公共 SDK 源码; - relayer:统一用户解密
/v3/user-decrypt端点的服务端实现(对应relayer/src/http下的 handler 与签名预检逻辑)。
八、总结
test-suite/e2e是 fhEVM 面向真实链环境的"体检中心":通过install-sdk.sh灵活地在本地 SDK 构建与 registry 发布版本之间切换依赖源;用eip712-unified-user-decrypt-v1信封直连 relayer/v3/user-decrypt,把签名验证(relayer 同步预检)、host-ACL(relayer 每任务检查)与allowedContracts/签名失效(KMS Connector 静默拒绝)三层拒绝路径用三种不同的断言方式区分开来;算子边界用例通过shiftSemantics.ts与 tfhe-rs 版本严格联动;冒烟运行器则以多签名者故障切换、费率抬升、backlog 取消与 BetterStack 心跳上报,为 Sepolia/Mainnet/Devnet 提供了可在无人值守 CI 中长期运行的链上健康检查。
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考