news 2026/9/12 15:55:43

使用 fhEVM 构建加密计数器:从普通 Solidity Counter 到全同态加密 FHECounter 的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 fhEVM 构建加密计数器:从普通 Solidity Counter 到全同态加密 FHECounter 的完整实战

使用 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 测试端的差异,并理解euint32externalEuint32FHE.fromExternalFHE.add/suballowThis/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(含euint32externalEuint32fromExternaladdsuballowallowThis等)
  • 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; } }

关键点:

  • _countuint32类型,直接以明文存储在链上,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 计数器,本质变化有四处:

维度普通 CounterFHE 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 中,euint32type(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 行:allowallowThis都会先对未初始化值做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)的完整调用链如下:

  1. 客户端(TS 测试)fhevm.createEncryptedInput(addr, alice).add32(1).encrypt()在本地把明文1加密为密文,输出handles[0](密文句柄)与inputProof(证明);
  2. 客户端 → 合约:alice 调用FHECounter.increment(handles[0], inputProof),密文与证明上链;
  3. 合约内FHE.fromExternal(handle, proof)验证证明并转为euint32FHE.add(_count, encrypted)在加密域内完成加法生成新句柄;FHE.allowThis+FHE.allow(_count, msg.sender)授予合约与 alice 后续使用/解密权限;
  4. 链下解密:alice 调用fhevm.userDecryptEuint(...),在拥有allow权限的前提下,通过 KMS / coprocessor 服务将密文解密回明文1,用于断言。

从仓库源码看,FHE.add/FHE.sub最终都落到 library-solidity/lib/Impl.sol 的底层实现(Impl.addImpl.subImpl.verifyImpl.allow),由 fhEVM 的预编译/coprocessor 基础设施在链上执行同态运算与权限校验。


6. 运行方式与前提条件

  • 环境前提:需要配置好 fhEVM 的 Hardhat 开发环境(含@fhevm/hardhat-plugin等),并在本地拉起支持 FHE 的节点(如 Anvil + coprocessor 或 fhEVM 测试网络,chainId 31337 本地网络即可满足ZamaEthereumConfig的配置路由)。
  • 文件放置:严格按照第 2 节要求,把Counter.solFHECounter.sol放入contracts/,把counter.tsfheCounter.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 中继续阅读fheaddfheifthenelsesealed-bid-auctionheads-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),仅供参考

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

机器人软件开发中的边界测试:集成测试的核心技术与实践探讨

在现代技术开发领域,机器人软件开发正迅速崛起,它融合了硬件与软件的复杂性,为自动化、智能制造、服务业等提供了广阔前景。作为一名开发人员,我深知测试环节的重要性,它确保了软件在真实环境中的稳健性。在集成测试这个关键领域,我们聚焦于几个子过程:模块联调负责组件…

作者头像 李华
网站建设 2026/9/12 15:48:45

RAG系统在动态评测中的挑战与记忆型智能体优势

1. 项目概述&#xff1a;当RAG遇上动态评测的挑战最近在AGI评测领域出现了一个值得关注的现象&#xff1a;传统RAG&#xff08;检索增强生成&#xff09;系统在新型动态评测框架AMemGym中的表现出现了出人意料的排名倒退&#xff0c;而记忆型智能体却实现了性能逆袭。这个发现直…

作者头像 李华
网站建设 2026/9/12 15:47:19

Flask电商推荐系统:Python实现与工程实践

1. 项目概述与核心价值这个基于Flask框架的电子商务消费者推荐系统&#xff08;源码编号43733&#xff09;是一个典型的计算机专业毕业设计项目&#xff0c;它瞄准了电商领域最核心的痛点——如何在海量商品中实现精准的个性化推荐。我在实际电商系统开发中发现&#xff0c;一个…

作者头像 李华