news 2026/9/13 4:10:42

在 Hardhat 中编写 FHEVM 测试:使用 FHEVM Hardhat Plugin 实现加密输入与用户解密

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 Hardhat 中编写 FHEVM 测试:使用 FHEVM Hardhat Plugin 实现加密输入与用户解密

在 Hardhat 中编写 FHEVM 测试:使用 FHEVM Hardhat Plugin 实现加密输入与用户解密

【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm

本篇指南聚焦于在 Hardhat 测试工程中,借助FHEVM Hardhat Plugin为全同态加密智能合约编写测试的核心技能:如何在 TypeScript 测试代码中启用插件、通过fhevm运行时模块对输入值做本地加密、构造externalEuintXX密文句柄与零知识证明,再调用合约方法并最终用userDecryptEuint系列 API 解密链上密文进行断言。读完本文,你将能够在 fhevm 仓库所描述的 FHEVM 开发体系中,从零写出可运行、可验证的加密合约测试,并理解句柄(handle)、输入证明(inputProof)与 FHE 权限在其中的底层作用。

前置条件:启用 FHEVM Hardhat Plugin

FHEVM 的测试能力由一个独立的 Hardhat 插件提供,其作用与任何普通 Hardhat 插件一致:在加载阶段被引入,从而把 FHEVM 相关能力注入 Hardhat 运行时环境(Hardhat Runtime Environment,简称 HRE)。要启用它,只需在hardhat.config.ts中添加一行 import:

import "@fhevm/hardhat-plugin";

⚠️注意:如果没有这行 import,Hardhat 运行时环境中将不会存在 FHEVM API,后续所有加密、解密调用都会因找不到fhevm模块而失败。因此这行 import 是编写一切 FHEVM 测试的前提。

关于整个开发环境的初始化(例如 Node.js 版本要求——建议使用偶数版本如v18.xv20.x,以及 FHEVM Hardhat 模板仓库的创建与npm install步骤),可以参阅 Hardhat 环境搭建指南;插件能力的整体介绍见 Hardhat 插件开发指南。

访问 Hardhat FHEVM API

插件在启用后,会向标准 Hardhat Runtime Environment 扩展一个新的fhevm模块。在测试代码中有两种等价的方式拿到它:

import { fhevm } from "hardhat";

import * as hre from "hardhat"; // Then access: hre.fhevm

两种写法都指向同一个运行时单例。后续所有加密与解密操作,例如fhevm.createEncryptedInput(...)fhevm.userDecryptEuint(...),都从该模块发起。这也是整个 FHEVM Hardhat 测试 API 的入口。

加密输入:在测试中构造并提交密文

FHEVM 的核心使用场景是:测试方(例如用户 Alice)把明文值在本地加密成密文,提交给链上合约处理。链上合约处理的是密文本身,因此明文在链上任何环节都不会出现。

Solidity 侧的函数签名

假设被测合约有一个名为foo的函数,接收一个加密的uint32。按 FHEVM 规范,Solidity 侧应这样声明:

function foo(externalEuint32 value, bytes calldata inputProof);

其中:

  • externalEuint32 value:一个bytes32,表示加密后的uint32,即加密输入的句柄(handle)externalEuint32是 FHEVM 对外部加密输入的专用类型,与合约内部使用的euint32相区分——它表明该密文来自链下用户,必须经过完整性验证后才能进入合约计算。
  • bytes calldata inputProofbytes数组,保存验证该加密有效性的零知识证明(Zero-Knowledge Proof of Knowledge,ZKPoK)

关于externalEuintXX/externalEbool/externalEaddress类型与bytes inputProof参数的完整设计说明,可进一步阅读 加密输入(Encrypted Inputs)文档。

TypeScript 侧的加密四步流程

在 TypeScript 测试中,计算这两个参数需要准备两样东西:

  • 目标合约的地址contractAddress
  • 签名者的地址(即发送交易的账户,如signers.alice.address

随后按如下四步完成加密并调用:

第 1 步:创建一个新的加密输入对象

// use the `fhevm` API module from the Hardhat Runtime Environment const input = fhevm.createEncryptedInput(contractAddress, signers.alice.address);

createEncryptedInput返回一个加密输入构造器,它把密文与「合约地址 + 用户地址」双重绑定:生成的密文只能由该用户在指定合约中使用。

第 2 步:添加要加密的值

input.add32(12345);

add32对应加密一个uint32。FHEVM 还提供add8add16add64addBooladdAddress等按位宽区分的追加方法,用于在同一输入对象上打包多个不同类型的加密值。

第 3 步:执行本地加密

const encryptedInputs = await input.encrypt();

encrypt()是异步操作:它在本地完成 FHE 公钥加密,并生成用于链上验证的零知识证明。

第 4 步:调用 Solidity 函数

const externalUint32Value = encryptedInputs.handles[0]; const inputProof = encryptedInputs.inputProof; const tx = await input.foo(externalUint32Value, inputProof); await tx.wait();

encryptedInputs.handles是一个数组,按add32等方法的调用顺序保存各个加密值的bytes32句柄;encryptedInputs.inputProof则是对应整个打包结果的 ZKPoK。二者分别填入函数的两个参数即可。

💡 补充说明:多个加密值可以打包进同一个输入对象。例如 加密输入文档 中的例子依次调用addBool(...)add64(...)add8(...),加密结果通过handles[0]handles[1]handles[2]按添加顺序取出,再分别传给 Solidity 函数的多个externalEbool/externalEuint64/externalEuint8参数。TypeScript 侧的添加顺序与 Solidity 函数参数的声明顺序没有强制对应关系,开发时可以自由组织。

仓库中的真实用法佐证

仓库中大量真实测试都遵循上述模式。以 EncryptedERC20 测试 为例,transfer测试先用createEncryptedInput为 Alice 构造加密转账金额,再取出句柄与证明调用合约:

const input = this.instances.alice.createEncryptedInput(this.contractAddress, this.signers.alice.address); input.add64(1337); const encryptedTransferAmount = await input.encrypt(); const tx = await this.erc20'transfer(address,bytes32,bytes)';

同一测试文件中,还对句柄的字节结构做了校验(见 EncryptedERC20.ts 的 mint 测试):句柄的字节 21 被置为0xff,字节 22~29 编码链 ID(chainId),字节 30 编码 FHE 类型(如euint64对应05),字节 31 为句柄版本。这说明一个bytes32句柄并非随机数,而是携带了类型、链与版本信息的 FHEVM 内部引用。

更多加密示例

仓库文档目录还提供了从简单到完整的加密示例,可与本节对照学习:

  • 单值加密示例
  • 多值加密示例
  • FHECounter 完整示例

解密:用 userDecryptEuint 系列 API 读取明文

加密值进入合约参与计算后,测试方需要把它读出来并解密成明文做断言。以用户Alice解密合约中一个euint32值为例,合约需暴露如下view函数:

function getEncryptedUint32Value() public view returns (euint32) { returns _encryptedUint32Value; }

⚠️权限前提:为简化说明,这里假设 Alice 的账户与目标合约都已经具备解密该值所需的 FHE 权限。FHE 权限的具体工作机制(allow/ ACL)请参阅 ACL 文档 以及 用户解密委托说明。如果目标合约或用户任一方没有 FHE 权限,解密调用将直接失败。

解密分两步进行:

第 1 步:从合约读取加密值(一个bytes32句柄)

const encryptedUint32Value = await contract.getEncryptedUint32Value();

第 2 步:调用 FHEVM API 执行解密

const clearUint32Value = await fhevm.userDecryptEuint( FhevmType.euint32, // Encrypted type (must match the Solidity type) encryptedUint32Value, // bytes32 handle Alice wants to decrypt contractAddress, // Target contract address signers.alice, // Alice’s wallet );

userDecryptEuint的四个参数含义分别为:

  1. FhevmType:加密值的整数类型,必须与 Solidity 侧类型严格一致(例如euint32对应FhevmType.euint32)。类型不匹配会导致解密失败。
  2. 加密句柄:要解密的bytes32句柄。
  3. 合约地址:持有该句柄访问权限的目标合约地址。
  4. 用户签名者:拥有该句柄访问权限的用户钱包(如signers.alice)。

支持的解密类型

FHEVM 为每种加密类型提供了对应的解密函数,使用时按下表选择:

类型函数
euintXXXfhevm.userDecryptEuint(...)
eboolfhevm.userDecryptEbool(...)
eaddressfhevm.userDecryptEaddress(...)

FHEVM 支持的加密类型全集(ebooleuint8euint256eaddress等)及各自的位宽与支持算子,见 支持的加密类型文档。

权限失败的真实表现

权限约束在真实测试中是可以被验证的。EncryptedERC20.ts 专门断言了「Bob 无法解密密文」这一负向场景:当 Bob 尝试对 Alice 的余额句柄执行解密/重加密时,会抛出User is not authorized to reencrypt this handle!异常。这印证了文档中「权限缺失导致解密失败」的警告——测试编写者可以把这类断言纳入自己的测试,以覆盖安全边界。

更多解密示例

  • 单值用户解密示例
  • 多值用户解密示例

从零搭建一个完整的 FHEVM 测试文件

把加密与解密串起来,一个完整的 FHEVM 测试文件骨架大致如下。以仓库文档 Test the FHEVM contract 中的FHECounter为例:

import { FHECounter, FHECounter__factory } from "../types"; import { FhevmType } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers, fhevm } from "hardhat"; type Signers = { deployer: HardhatEthersSigner; alice: HardhatEthersSigner; bob: HardhatEthersSigner; }; async function deployFixture() { const factory = (await ethers.getContractFactory("FHECounter")) as FHECounter__factory; const fheCounterContract = (await factory.deploy()) as FHECounter; const fheCounterContractAddress = await fheCounterContract.getAddress(); return { fheCounterContract, fheCounterContractAddress }; } describe("FHECounter", function () { let signers: Signers; let fheCounterContract: FHECounter; let fheCounterContractAddress: string; before(async function () { const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { deployer: ethSigners[0], alice: ethSigners[1], bob: ethSigners[2] }; }); beforeEach(async () => { ({ fheCounterContract, fheCounterContractAddress } = await deployFixture()); }); it("encrypted count should be uninitialized after deployment", async function () { const encryptedCount = await fheCounterContract.getCount(); // 部署后初始加密计数应为 bytes32(0),表示尚未初始化 expect(encryptedCount).to.eq(ethers.ZeroHash); }); it("increment the counter by 1", async function () { const encryptedCountBeforeInc = await fheCounterContract.getCount(); expect(encryptedCountBeforeInc).to.eq(ethers.ZeroHash); const clearCountBeforeInc = 0; // 本地加密常量 1 为 euint32 const clearOne = 1; const encryptedOne = await fhevm .createEncryptedInput(fheCounterContractAddress, signers.alice.address) .add32(clearOne) .encrypt(); // 以加密参数调用 increment const tx = await fheCounterContract.connect(signers.alice).increment(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait(); const encryptedCountAfterInc = await fheCounterContract.getCount(); const clearCountAfterInc = await fhevm.userDecryptEuint( FhevmType.euint32, encryptedCountAfterInc, fheCounterContractAddress, signers.alice, ); expect(clearCountAfterInc).to.eq(clearCountBeforeInc + clearOne); }); });

这段代码体现了与普通 Hardhat 测试的几个关键差异,值得逐一理解:

  1. 句柄而非数值getCount()返回的不再是 TypeScriptnumber,而是一个十六进制bytes32字符串(FHEVM 句柄),指向类型为euint32的加密原语。未初始化时它等于0x0000...0000(即ethers.ZeroHash),不引用任何加密值。
  2. 加密输入绑定上下文fhevm.createEncryptedInput(contractAddress, signers.alice.address)生成的密文同时绑定合约地址与用户地址,只能由 Alice 在该合约中使用,不能被其他用户或其他合约复用,从而保证数据机密性与上下文绑定。
  3. 入参数量变化increment()由普通合约的单参数变为increment(encryptedOne.handles[0], encryptedOne.inputProof)双参数。这是因为 FHEVM 除密文句柄外,还需要附带 ZKPoK 来证明该加密输入与调用者(Alice)以及目标合约绑定,防止密文在异构上下文被重放。
  4. 解密需要类型与权限双重匹配userDecryptEuintFhevmType.euint32必须与 Solidity 类型一致;句柄的访问权限由链上FHE.allow()等机制授权。

在三种运行时模式下执行测试

FHEVM Hardhat Plugin 提供三种运行时模式,对应合约开发与测试的不同阶段,在速度、加密强度与持久性之间取舍:

模式加密方式持久化链环境速度适用场景
Hardhat(默认)模拟加密内存网络非常快常规测试、CI 覆盖率、早期合约开发的快速反馈
Hardhat Node模拟加密本地服务器前端联调、模拟用户流程、本地持久化测试
Sepolia 测试网真实加密链上服务器全栈验证,唯一使用真实加密值的模式

默认内存模式下,直接执行:

npx hardhat test --network hardhat

本地节点、Sepolia 部署与交互的完整操作步骤(包括npx hardhat nodenpx hardhat deploy --network localhostnpx hardhat fhevm check-fhevm-compatibility以及task:decrypt-counttask:increment等任务用法),参见 部署合约并运行测试。

补充提示:如果你的测试逻辑需要在自定义 Hardhat Task(而非test/compile内置任务)中使用 FHEVM API,必须在任务开头显式调用fhevm.initializeCLIApi(),因为自定义任务不会像内置任务那样自动初始化 FHEVM 运行时环境。具体写法参见 编写 FHEVM Hardhat 任务。

实战要点与常见陷阱

  • 忘记 import 插件hardhat.config.ts缺少import "@fhevm/hardhat-plugin";时,hre.fhevm不存在,所有加密解密调用都会抛错。这是最高频的入门错误。
  • 类型不匹配FhevmType.euint32与 Solidity 的euint32必须一一对应;句柄的编码(字节 30)本身就携带类型信息,类型错配时解密会失败或产生错误结果。
  • 权限缺失:合约与用户都必须拥有句柄的 FHE 权限(通过FHE.allow()等链上机制授予),否则解密抛错。可参考仓库测试 EncryptedERC20.ts 的负向断言写法。
  • 未初始化句柄为 ZeroHash:合约中尚未赋值的euint变量其句柄为全零bytes32,在测试中应先用ethers.ZeroHash断言,再进入加密计算流程。
  • 句柄与证明的配对handles[i]inputProof来自同一次encrypt()调用,跨输入对象混用会导致链上零知识验证失败。
  • 覆盖权限与非法调用等负向路径:FHEVM 的安全边界(如其他用户越权解密、密文跨合约复用)本身就是重要测试点,仓库测试中已有成熟范例可参考。

通过以上步骤,你就掌握了 FHEVM 合约测试从「启用插件 → 构造加密输入 → 调用合约 → 解密断言」的完整闭环。进一步深入学习可参阅 FHEVM Solidity 指南总目录、加密输入文档 与 FHEVM API 参考。

【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 4:10:15

RIAV-MVS:非对称代价体与循环索引如何重塑多视角立体深度估计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 4:09:48

串口通信全链路排障:从物理层到Python实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 4:08:59

Oracle 19c RAC实战:Linux环境下的集群安装与踩坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华