使用 fhEVM 构建加密计数器:从普通 Solidity Counter 到全同态加密 FHECounter 的完整实战
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
导读
本文以 docs/examples/fhe-counter.md 为骨架,逐步演示如何在 fhEVM(Fully Homomorphic Encryption Virtual Machine)上,将一个传统的、明文的Counter合约改造成一个全程密文运算的FHECounter合约。你将从零搭起可编译、可测试的 Hardhat 工程结构,对比普通计数与加密计数在合约端与 TypeScript 测试端的差异,并理解euint32、externalEuint32、FHE.fromExternal、FHE.add/sub、allowThis/allow这些核心 API 的真实语义。读完本文,你将具备独立编写并验证一个基于 fhEVM 的密文状态机应用的基础能力。
1. 示例背景:为什么计数器也要加密?
普通智能合约中的状态变量(例如uint32 _count)以明文存储在所有节点上,任何人通过区块链浏览器即可读取其当前值。当计数对象涉及敏感业务(如投票数、竞价金额、用户积分、隐私偏好统计)时,明文存储就构成了数据泄露风险。
fhEVM 通过全同态加密(FHE)让合约在不解密的前提下对密文执行加、减、乘、比较等运算。本文将普通计数器升级为加密计数器的过程,完整展示了从"明文可读状态"到"密文状态 + 受控解密"的迁移路径,是学习 fhEVM 最经典的入门案例。
1.1 本示例在仓库中的位置
关联文档 docs/examples/fhe-counter.md 位于仓库 docs 的 examples 目录下,与其同级的还有 fheadd、fheifthenelse、heads-or-tails、sealed-bid-auction 等示例。本文用到的核心依赖都来自本仓库:
- FHE 类型与运算库:library-solidity/lib/FHE.sol(含
euint32、externalEuint32、fromExternal、add、sub、allow、allowThis等) - Zama 网络配置库:library-solidity/config/ZamaConfig.sol
- 仓库内另一个更简化的明文 Counter 示例:library-solidity/examples/Counter.sol
提示:原文档与仓库中的 FHE 合约示例位于不同子项目,本文以关联文档中的
Counter.sol/FHECounter.sol为讲解主线,仓库源码作为实现佐证。文中涉及的@fhevm/solidity等包名与仓库实际发布形态可能存在细微差异,请以你所安装的 fhEVM 工具链版本为准。
2. 工程结构要求:文件放对位置才能跑起来
原文档在开头特别强调了一个极易踩坑的目录约束:
.sol合约文件 → 必须放在<your-project-root-dir>/contracts/.ts测试文件 → 必须放在<your-project-root-dir>/test/
只有满足这一目录结构,Hardhat 才能正常编译合约并发现测试。因此一个最小可运行的工程目录大致为:
<your-project-root-dir>/ ├── contracts/ │ ├── Counter.sol # 普通计数器 │ └── FHECounter.sol # FHE 加密计数器 ├── test/ │ ├── counter.ts # 普通计数器测试 │ └── fheCounter.ts # FHE 计数器测试 ├── hardhat.config.ts # 集成 fhevm 插件 └── package.json这个结构与仓库中 library-solidity 子项目(examples/放合约、test/放 TS 测试)以及 test-suite/e2e(contracts/与test/分离)的组织方式一致,说明"合约目录与测试目录分离"是 fhEVM 官方示例的通用约定。
3. 一个普通的 Counter(明文版)
3.1 合约代码counter.sol
// SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; /// @title A simple counter contract contract Counter { uint32 private _count; /// @notice Returns the current count function getCount() external view returns (uint32) { return _count; } /// @notice Increments the counter by a specific value function increment(uint32 value) external { _count += value; } /// @notice Decrements the counter by a specific value function decrement(uint32 value) external { require(_count >= value, "Counter: cannot decrement below zero"); _count -= value; } }关键点:
_count是uint32类型,直接以明文存储在链上,getCount()返回明文;increment(value)/decrement(value)接收明文 uint32参数,直接对状态变量做+=/-=;- 由于减法在无符号整数上可能下溢,这里用
require(_count >= value, ...)做了防御性检查。
仓库中的 library-solidity/examples/Counter.sol 是一个更简化的同款示例(increment()固定加 1、currentValue()读值),可作为对照参考。
3.2 测试代码counter.ts
import { Counter, Counter__factory } from "../types"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; type Signers = { deployer: HardhatEthersSigner; alice: HardhatEthersSigner; bob: HardhatEthersSigner; }; async function deployFixture() { const factory = (await ethers.getContractFactory("Counter")) as Counter__factory; const counterContract = (await factory.deploy()) as Counter; const counterContractAddress = await counterContract.getAddress(); return { counterContract, counterContractAddress }; } describe("Counter", function () { let signers: Signers; let counterContract: Counter; before(async function () { const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { deployer: ethSigners[0], alice: ethSigners[1], bob: ethSigners[2] }; }); beforeEach(async () => { ({ counterContract } = await deployFixture()); }); it("count should be zero after deployment", async function () { const count = await counterContract.getCount(); console.log(`Counter.getCount() === ${count}`); // Expect initial count to be 0 after deployment expect(count).to.eq(0); }); it("increment the counter by 1", async function () { const countBeforeInc = await counterContract.getCount(); const tx = await counterContract.connect(signers.alice).increment(1); await tx.wait(); const countAfterInc = await counterContract.getCount(); expect(countAfterInc).to.eq(countBeforeInc + 1n); }); it("decrement the counter by 1", async function () { // First increment, count becomes 1 let tx = await counterContract.connect(signers.alice).increment(1); await tx.wait(); // Then decrement, count goes back to 0 tx = await counterContract.connect(signers.alice).decrement(1); await tx.wait(); const count = await counterContract.getCount(); expect(count).to.eq(0); }); });这段测试完全使用标准 ethers.js / Hardhat 模式,没有任何 FHE 相关 API。值得注意的是:
getCount()是view调用,读回的即是真实明文,断言直接比较数值;- 用
signers.alice连接合约执行写操作,模拟不同账户的操作权限; - 测试覆盖了三个典型场景:部署后为 0、自增 1、自增后再自减回到 0。
4. 一个 FHE 计数器(密文版)
4.1 核心差异一览
将普通计数器升级为 FHE 计数器,本质变化有四处:
| 维度 | 普通 Counter | FHE Counter |
|---|---|---|
| 状态变量类型 | uint32(明文) | euint32(密文句柄) |
| 写入参数 | uint32 value(明文) | externalEuint32 inputEuint32 + bytes inputProof(外部密文 + 证明) |
| 运算方式 | 原生+=/-= | FHE.add(...)/FHE.sub(...) |
| 读取方式 | view直接返回明文 | 返回euint32密文句柄,需客户端解密 |
| 权限控制 | 无 | FHE.allowThis/FHE.allow授予解密/使用权限 |
4.2 合约代码FHECounter.sol
// SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, euint32, externalEuint32 } from "@fhevm/solidity/lib/FHE.sol"; import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /// @title A simple FHE counter contract contract FHECounter is ZamaEthereumConfig { euint32 private _count; /// @notice Returns the current count function getCount() external view returns (euint32) { return _count; } /// @notice Increments the counter by a specified encrypted value. /// @dev This example omits overflow/underflow checks for simplicity and readability. /// In a production contract, proper range checks should be implemented. function increment(externalEuint32 inputEuint32, bytes calldata inputProof) external { euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); _count = FHE.add(_count, encryptedEuint32); FHE.allowThis(_count); FHE.allow(_count, msg.sender); } /// @notice Decrements the counter by a specified encrypted value. /// @dev This example omits overflow/underflow checks for simplicity and readability. /// In a production contract, proper range checks should be implemented. function decrement(externalEuint32 inputEuint32, bytes calldata inputProof) external { euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); _count = FHE.sub(_count, encryptedEuint32); FHE.allowThis(_count); FHE.allow(_count, msg.sender); } }下面逐行拆解这个合约的每个关键环节,并结合仓库源码印证其真实行为。
4.2.1 继承ZamaEthereumConfig:拿到当前链的 FHEVM 配置
合约声明contract FHECounter is ZamaEthereumConfig。在仓库中,ZamaConfig.sol 的getCoprocessorConfig()按block.chainid路由返回当前链上 ACL、Coprocessor、KMSVerifier 等核心合约地址:Ethereum mainnet(chainId=1)、Polygon(137)、Sepolia(11155111)、Polygon Amoy(80002)以及本地 Hardhat/Anvil 网络(31337);在其他链上会revert ZamaProtocolUnsupported()。也就是说,同一个合约可以不改代码地在上述网络中部署运行,前提是继承对应的 Zama 配置基类。
4.2.2euint32 private _count:密文状态变量
euint32是"32 位无符号整数的密文句柄"类型。在 library-solidity/lib/FHE.sol 中,euint32是type(uint256).wrap的用户定义值类型(user-defined value type),链上存储的其实是底层密文句柄(bytes32形式),真正的明文数值从未离开过加密环境。文档中的注释也点明:部署后getCount()返回的是bytes32(0),即"未初始化"状态。
4.2.3externalEuint32 + inputProof:外部密文输入与证明
increment/decrement不接收明文,而是接收externalEuint32 inputEuint32(调用者本地加密后产生的密文句柄)和bytes calldata inputProof(证明该密文确实由合法的客户端加密密钥生成)。这一设计保证了任何人都不能伪造一个看似加密的输入注入合约——密文必须通过 fhEVM 官方的加密库生成并附带可验证的证明。
4.2.4FHE.fromExternal:验证并接入外部密文
euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof);对照 library-solidity/lib/FHE.sol 中fromExternal(externalEuint32, bytes)的实现(第 8608 行附近):
- 当
inputProof非空时,调用Impl.verify(...),用证明校验输入密文; - 当
inputProof为空时,若句柄为 0 则视为明文 0,否则要求该句柄已通过allow授权给msg.sender(否则revert SenderNotAllowedToUseHandle),这一路径为智能合约账户(smart contract account)集成 fhEVM 提供了可能。
4.2.5FHE.add/FHE.sub:在密文上做同态运算
_count = FHE.add(_count, encryptedEuint32); _count = FHE.sub(_count, encryptedEuint32);在 library-solidity/lib/FHE.sol 中,add(euint32, euint32)(第 2528 行)与sub(euint32, euint32)(第 2541 行)的实现会先将未初始化的操作数视为 0(通过isInitialized检查后asEuint32(0)),再调用底层Impl.add/Impl.sub生成新的密文句柄。也就是说密文是在链上、由 coprocessor 在加密域内完成加法/减法的,任何人(包括合约本身)都无法看到中间数值。
4.2.6FHE.allowThis/FHE.allow:解密与使用授权
FHE.allowThis(_count); FHE.allow(_count, msg.sender);FHE.allowThis(_count)把新生成的密文句柄授权给合约自身,供后续合约内继续运算;FHE.allow(_count, msg.sender)把句柄授权给当前调用者(如 alice),使 alice 可以在链下请求解密该值。
对照 library-solidity/lib/FHE.sol 第 9352~9369 行:allow与allowThis都会先对未初始化值做asEuint32(0)兜底,再调用Impl.allow(...)写入授权关系。这是 fhEVM细粒度解密权限控制的体现:谁被allow,谁才有资格拿到解密后的明文,状态本身永远不公开。
4.2.7 关于溢出/下溢:文档明确的取舍
两个函数都带有@dev注释,原文明确写道:
"This example omits overflow/underflow checks for simplicity and readability. In a production contract, proper range checks should be implemented."
即示例为了可读性省略了溢出/下溢检查,生产环境必须自行补充范围校验(例如用FHE.gte等比较运算在密文域内做下溢保护)。这一点应视为原文档对读者的显式安全提醒。
4.3 测试代码FHECounter.ts
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(); // Expect initial count to be bytes32(0) after deployment, // (meaning the encrypted count value is uninitialized) 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; // Encrypt constant 1 as a euint32 const clearOne = 1; const encryptedOne = await fhevm .createEncryptedInput(fheCounterContractAddress, signers.alice.address) .add32(clearOne) .encrypt(); 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); }); it("decrement the counter by 1", async function () { // Encrypt constant 1 as a euint32 const clearOne = 1; const encryptedOne = await fhevm .createEncryptedInput(fheCounterContractAddress, signers.alice.address) .add32(clearOne) .encrypt(); // First increment by 1, count becomes 1 let tx = await fheCounterContract .connect(signers.alice) .increment(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait(); // Then decrement by 1, count goes back to 0 tx = await fheCounterContract.connect(signers.alice).decrement(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait(); const encryptedCountAfterDec = await fheCounterContract.getCount(); const clearCountAfterDec = await fhevm.userDecryptEuint( FhevmType.euint32, encryptedCountAfterDec, fheCounterContractAddress, signers.alice, ); expect(clearCountAfterDec).to.eq(0); }); });4.3.1 从hardhat导入fhevm:插件注入的 FHE 环境
与普通测试相比,这里额外从hardhat导入了fhevm对象(并引入FhevmType枚举)。这是 fhEVM Hardhat 插件为测试环境注入的客户端环境,负责在测试进程内完成密文的创建、上链后的解密。仓库中 library-solidity/test/fhevmOperations/manual.ts 等测试同样使用createEncryptedInput(...)构造加密输入,模式完全一致。
4.3.2createEncryptedInput(...).add32(1).encrypt():客户端加密
const encryptedOne = await fhevm .createEncryptedInput(fheCounterContractAddress, signers.alice.address) .add32(clearOne) .encrypt();createEncryptedInput(contractAddress, accountAddress)创建针对目标合约、以指定账户身份加密的输入构造器;.add32(clearOne)声明要加密一个uint32类型的明文值 1,并生成对应的euint32密文;.encrypt()完成本地加密,返回{ handles, inputProof }:encryptedOne.handles[0]是加密结果的密文句柄,传给合约的externalEuint32参数;encryptedOne.inputProof是对应证明,传给bytes inputProof参数。
4.3.3userDecryptEuint:受控解密并断言明文
const clearCountAfterInc = await fhevm.userDecryptEuint( FhevmType.euint32, encryptedCountAfterInc, fheCounterContractAddress, signers.alice, );userDecryptEuint以"用户"身份发起解密请求(本测试中即 alice),配合合约内FHE.allow(_count, msg.sender)授予的权限,把链上的密文句柄解密回明文。整个测试对明文的断言逻辑与普通 Counter 完全一致(expect(clearCountAfterInc).to.eq(clearCountBeforeInc + clearOne)),但所有读写中间过程都发生在密文域。
4.3.4 测试用例与普通版的对应关系
| 普通 Counter 测试 | FHE Counter 测试 | 差异点 |
|---|---|---|
部署后getCount() == 0 | 部署后getCount() == ethers.ZeroHash | 未初始化的密文句柄是bytes32(0),不能期望返回明文 0 |
increment(1)后getCount() == 1 | 加密 1 → 调用increment(handle, proof)→ 解密后为 1 | 入参从明文变成"密文 + 证明" |
increment(1)再decrement(1)回到 0 | 同样步骤,全程密文运算,解密后为 0 | 同态减在密文域完成 |
注意第一行的差异非常重要:在 fhEVM 中,未初始化的密文状态读取到的是bytes32(0)句柄,而非数值 0,这是新手最容易困惑的地方。
5. 原理解读:这条密文路径上发生了什么
结合合约与测试,一次increment(1)的完整调用链如下:
- 客户端(TS 测试):
fhevm.createEncryptedInput(addr, alice).add32(1).encrypt()在本地把明文1加密为密文,输出handles[0](密文句柄)与inputProof(证明); - 客户端 → 合约:alice 调用
FHECounter.increment(handles[0], inputProof),密文与证明上链; - 合约内:
FHE.fromExternal(handle, proof)验证证明并转为euint32;FHE.add(_count, encrypted)在加密域内完成加法生成新句柄;FHE.allowThis+FHE.allow(_count, msg.sender)授予合约与 alice 后续使用/解密权限; - 链下解密:alice 调用
fhevm.userDecryptEuint(...),在拥有allow权限的前提下,通过 KMS / coprocessor 服务将密文解密回明文1,用于断言。
从仓库源码看,FHE.add/FHE.sub最终都落到 library-solidity/lib/Impl.sol 的底层实现(Impl.add、Impl.sub、Impl.verify、Impl.allow),由 fhEVM 的预编译/coprocessor 基础设施在链上执行同态运算与权限校验。
6. 运行方式与前提条件
- 环境前提:需要配置好 fhEVM 的 Hardhat 开发环境(含
@fhevm/hardhat-plugin等),并在本地拉起支持 FHE 的节点(如 Anvil + coprocessor 或 fhEVM 测试网络,chainId 31337 本地网络即可满足ZamaEthereumConfig的配置路由)。 - 文件放置:严格按照第 2 节要求,把
Counter.sol、FHECounter.sol放入contracts/,把counter.ts、fheCounter.ts放入test/。 - 执行测试:在工程根目录运行 Hardhat 测试命令(如
npx hardhat test),应看到两个describe块共 6 个用例全部通过。 - 验证标准:普通 Counter 的
getCount()直接返回数值;FHECounter 的getCount()返回密文句柄(初始为ethers.ZeroHash),只有通过userDecryptEuint并拥有allow授权的账户才能拿到明文。
仓库内 library-solidity、test-suite/e2e 等子项目提供了大量同模式的可运行测试(如 library-solidity/test/fhevmOperations/manual.ts 中的createEncryptedInput+ 解密断言),可作为进一步学习的参照。
7. 小结与下一步
通过本文,你已经掌握了:
- 普通计数器与 FHE 计数器在状态类型、入参形态、运算方式、读取方式、权限模型上的五大差异;
euint32/externalEuint32的类型语义,以及FHE.fromExternal的验证逻辑(FHE.sol 第 8608 行起);FHE.add/FHE.sub对未初始化值的兜底处理(第 2528、2541 行)与allow/allowThis的授权机制(第 9352~9369 行);- 客户端
createEncryptedInput+userDecryptEuint的完整闭环测试写法。
进阶方向:在 docs/examples 中继续阅读fheadd、fheifthenelse、sealed-bid-auction、heads-or-tails等示例,理解条件运算(FHE.ifThenElse)、比较运算在密封拍卖、掷骰子等真实场景中的应用;生产化时务必参照 docs/solidity-guides 与 library-solidity/SECURITY.md,补齐范围校验与访问控制。
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考