ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FHEVM 合约测试实战:使用 Hardhat 为 FHECounter 编写并运行全同态加密单元测试

FHEVM 合约测试实战:使用 Hardhat 为 FHECounter 编写并运行全同态加密单元测试 FHEVM 合约测试实战使用 Hardhat 为 FHECounter 编写并运行全同态加密单元测试【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm导读本教程带你完成 FHEVMFully Homomorphic Encryption Virtual Machine合约测试的完整迁移将标准 Hardhat 测试套件Counter.ts逐步改造成 FHEVM 兼容版本FHECounter.ts并在此基础上循序渐进地加入加密输入构造、零知识证明校验、同态计算与链下解密等测试能力。读完本文后你将掌握 FHEVM Hardhat 测试的完整套路——从部署即断言的骨架测试到加密 → 传入链上 → 同态运算 → 解密断言的端到端加密测试闭环。本文是 Quick Start Tutorial 系列的第三篇前置内容为《Write a simple contract》编写标准Counter.sol与Counter.ts和《Turn it into FHEVM》将合约升级为支持 FHE 计算的FHECounter.sol。背景测试文件迁移的总体思路标准Counter.ts中测试直接读写普通uint32数值getCount()返回一个 TypeScriptnumberincrement(1)接收明文参数。而在 FHEVM 世界里链上存储的是密文句柄FHEVM handle——一个 32 字节的bytes32十六进制字符串指向链上某个加密的 FHEVM 原始类型如euint32即加密的uint32。因此FHECounter.ts的迁移遵循三条主线测试结构保持不变仍然使用before初始化签名者、beforeEach部署全新合约实例、it定义断言用例交互对象替换从Counter换成 FHEVM 兼容合约FHECounter并在测试中通过fhevm模块完成加密与解密旧测试注释保留原Counter的单元测试以注释形式保留在代码中方便对照迁移前后每一处差异。FHECounter.sol的完整实现与FHECounter.ts的完整测试代码可以在仓库的 docs/examples/fhe-counter.md 中直接对照查看本文后续所有代码均以该示例为准。搭建 FHEVM 测试环境第一步创建测试脚本test/FHECounter.ts进入项目test目录cd your-project-root-directory/test创建名为FHECounter.ts的新文件并粘贴以下 TypeScript 骨架代码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(should be deployed, async function () { console.log(FHECounter has been deployed at address ${fheCounterContractAddress}); // Test the deployed address is valid expect(ethers.isAddress(fheCounterContractAddress)).to.eq(true); }); // 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(); // 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); // }); });与Counter.ts有什么不同测试文件在结构上与原始Counter.ts高度相似但它使用的是 FHEVM 兼容智能合约FHECounter而非普通Counter为便于理解迁移过程原Counter的单元测试被保留为注释方便你对照每一部分在迁移到 FHEVM 时如何改写测试逻辑虽然保持不变但这个版本已经通过 FHEVM 库为链上密文计算做好准备——即后续那些直接操作机密值的测试。注意第 5 行import { ethers, fhevm } from hardhat;其中fhevm正是由FHEVM Hardhat 插件fhevm/hardhat-plugin注入 Hardhat 运行时环境HRE的新模块加密输入创建与链下解密都依赖它。插件的启用与 API 细节见 docs/solidity-guides/hardhat/write_test.md。第二步运行测试test/FHECounter.ts在项目根目录执行npx hardhat test输出FHECounter FHECounter has been deployed at address 0x7553CB9124f974Ee475E5cE45482F90d5B6076BC ✔ should be deployed 1 passing (1ms)到这里你的 Hardhat FHEVM 测试环境已经正确搭建完成。测试函数逐步迁移环境就绪后就可以开始测试合约函数了。下文将按照原Counter的测试用例顺序逐步迁移为 FHEVM 版本。第 1 步调用getCount()view 函数用以下 FHEVM 等价版本替换被注释掉的旧Counter测试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); });有什么不同encryptedCount不再是一个普通的 TypeScript number而是一个表示 Soliditybytes32值的十六进制字符串即FHEVM handle。该句柄指向一个euint32类型的加密 FHEVM 原始类型内部表示一个加密的 Solidityuint32此时encryptedCount等于0x0000000000000000000000000000000000000000000000000000000000000000即ethers.ZeroHash说明加密计数尚未初始化还没有引用任何加密值。这一行为与FHECounter.sol的实现一一对应合约内euint32 private _count;声明时默认值为零句柄getCount()直接返回该句柄见 docs/examples/fhe-counter.md 中的合约代码。运行测试npx hardhat test预期输出Counter Counter has been deployed at address 0x7553CB9124f974Ee475E5cE45482F90d5B6076BC ✔ should be deployed ✔ encrypted count should be uninitialized after deployment 2 passing (7ms)第 2 步搭建increment()函数单元测试我们将逐步把increment()的单元测试迁移到 FHEVM。首先要处理第一次自增前的计数状态如前所述初始计数值是等于零的bytes32意味着 FHEVMeuint32变量尚未初始化我们将其解释为底层明文值为 0。用以下代码替换旧Counter的被注释测试it(increment the counter by 1, async function () { const encryptedCountBeforeInc await fheCounterContract.getCount(); expect(encryptedCountBeforeInc).to.eq(ethers.ZeroHash); const clearCountBeforeInc 0; // const tx await counterContract.connect(signers.alice).increment(1); // await tx.wait(); // const countAfterInc await counterContract.getCount(); // expect(countAfterInc).to.eq(countBeforeInc 1n); });这一阶段先验证自增前密文句柄仍为未初始化状态并记录明文的初始计数clearCountBeforeInc 0供后续解密断言使用。第 3 步加密increment()函数的参数increment()函数接收一个参数计数器要增加的值。在最初的Counter.sol中这是一个明文的uint32。现在改为传入加密值使用 FHEVM 的externalEuint32原始类型。这样可以在不暴露链上输入值的前提下安全地递增计数器。说明这里使用externalEuint32而非常规euint32。这告诉 FHEVM该加密的uint32是在链外例如由用户提供的在被合约使用前必须先验证其完整性与真实性。用以下代码替换it(increment the counter by 1, async function () { const encryptedCountBeforeInc await fheCounterContract.getCount(); expect(encryptedCountBeforeInc).to.eq(ethers.ZeroHash); const clearCountBeforeInc 0; // const tx await counterContract.connect(signers.alice).increment(1); // await tx.wait(); // const countAfterInc await counterContract.getCount(); // expect(countAfterInc).to.eq(countBeforeInc 1n); });替换为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 counterContract.connect(signers.alice).increment(1); // await tx.wait(); // const countAfterInc await counterContract.getCount(); // expect(countAfterInc).to.eq(countBeforeInc 1n); });这里用到了 FHEVM Hardhat 插件提供的加密输入构造链路API 细节见 docs/solidity-guides/hardhat/write_test.md步骤代码作用创建加密输入fhevm.createEncryptedInput(contractAddress, signerAddress)创建一个绑定到特定合约地址与特定用户地址的加密输入加入明文值.add32(clearOne)指定要加密的 32 位整数明文 1本地加密.encrypt()在本地完成加密返回{ handles, inputProof }安全语义fhevm.createEncryptedInput(fheCounterContractAddress, signers.alice.address)创建的加密值同时绑定到合约fheCounterContractAddress和用户signers.alice.address。这意味着只有 Alice 可以使用该加密值且只能在该地址的FHECounter.sol合约内使用。它不能被其他用户或其他合约复用从而保证数据机密性并实现上下文绑定的加密。第 4 步使用加密参数调用increment()函数拿到加密参数后就可以用它调用increment()。注意更新后的increment()函数现在接收两个参数而非一个这是 FHEVM 的要求externalEuint32—— 加密值本身inputProof—— 一份配套的零知识知识证明Zero-Knowledge Proof of KnowledgeZKPoK用于验证加密输入被安全绑定到调用者Alice即交易签名者目标智能合约正在执行increment()的那个合约。这确保了加密值无法在另一个上下文或由另一个用户复用从而保证机密性与完整性。用以下代码替换// const tx await counterContract.connect(signers.alice).increment(1); // await tx.wait();替换为const tx await fheCounterContract.connect(signers.alice).increment(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait();注意handles是数组add32(clearOne)只添加了一个值因此取handles[0]如果你通过多个add调用一次性加密多个值则按添加顺序取handles[i]。inputProof是与整个加密输入批次对应的证明直接原样传入。此时计数器已经通过**全同态加密FHE**成功自增 1。下一步我们将读取更新后的加密计数值并在本地解密它。不过先快速跑一遍测试确认一切正常。运行测试npx hardhat test预期输出FHECounter FHECounter has been deployed at address 0x7553CB9124f974Ee475E5cE45482F90d5B6076BC ✔ should be deployed ✔ encrypted count should be uninitialized after deployment ✔ increment the counter by 1 3 passing (7ms)第 5 步调用getCount()并解密数值计数器已使用加密输入完成自增现在需要读取更新后的加密值并使用 FHEVM Hardhat 插件提供的userDecryptEuint函数在本地解密。userDecryptEuint接收四个参数参数含义本示例取值1.FhevmTypeFHE 加密值的整数类型必须与 Solidity 类型一致FhevmType.euint32计数器是uint322.Encrypted handle表示待解密加密值的 32 字节 FHEVM 句柄encryptedCountAfterInc3.Smart contract address有权访问该加密句柄的合约地址fheCounterContractAddress4.User signer有权访问该句柄的签名者signers.alice注意访问 FHEVM 句柄的权限是通过 Solidity 的FHE.allow()函数在链上设置的见FHECounter.sol。在 《Turn it into FHEVM》 中increment()和decrement()在每次更新_count后都会调用FHE.allowThis(_count)与FHE.allow(_count, msg.sender)授予两项权限前者授予合约自身后者授予调用者。缺少任何一项链下解密都会失败。用以下代码替换// const countAfterInc await counterContract.getCount(); // expect(countAfterInc).to.eq(countBeforeInc 1n);替换为const encryptedCountAfterInc await fheCounterContract.getCount(); const clearCountAfterInc await fhevm.userDecryptEuint( FhevmType.euint32, encryptedCountAfterInc, fheCounterContractAddress, signers.alice, ); expect(clearCountAfterInc).to.eq(clearCountBeforeInc clearOne);解密后的明文clearCountAfterInc应等于自增前的明文计数0加上加密输入的明文值1。整个断言链路清晰展示了 FHEVM 测试的核心模式链上拿到的是密文句柄本地解密后才能与明文期望值比较。除userDecryptEuint外插件还提供userDecryptEbool、userDecryptEaddress等对应不同加密类型的解密函数。运行测试npx hardhat test预期输出FHECounter FHECounter has been deployed at address 0x7553CB9124f974Ee475E5cE45482F90d5B6076BC ✔ should be deployed ✔ encrypted count should be uninitialized after deployment ✔ increment the counter by 1 3 passing (7ms)第 6 步调用decrement()函数与上一个测试类似现在用加密输入调用decrement()函数。用以下代码替换旧Counter的被注释测试// it(decrement the counter by 1, async function () { // // First increment, count becomes 1 // let tx await counterContract.connect(signers.alice).increment(); // 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); // });替换为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); });这段测试完整演示了加密输入复用的典型场景同一个encryptedOne密文与证明先后被传入increment()和decrement()。链上FHECounter.sol中decrement()与increment()对称FHE.fromExternal(inputEuint32, inputProof)校验并转换外部密文_count FHE.sub(_count, encryptedEuint32)执行同态减法再通过FHE.allowThis/FHE.allow授予解密权限见 docs/examples/fhe-counter.md。运行测试npx hardhat test预期输出FHECounter FHECounter has been deployed at address 0x7553CB9124f974Ee475E5cE45482F90d5B6076BC ✔ should be deployed ✔ encrypted count should be uninitialized after deployment ✔ increment the counter by 1 ✔ decrement the counter by 1 4 passing (7ms)深入理解FHEVM 测试的运行模式与底层依据上述测试默认运行在 Hardhat 的内存网络上。FHEVM Hardhat 插件实际上提供了三种运行模式适用于不同开发阶段详见 docs/solidity-guides/hardhat/run_test.md模式加密方式持久化链速度适用场景Hardhat默认模拟加密Mock否内存非常快常规测试、CI 覆盖率、早期快速反馈Hardhat Node本地节点模拟加密Mock是本地服务器快前端交互测试、用户流程模拟、本地持久化部署验证Sepolia 测试网真实加密是服务器慢合约逻辑稳定后的全栈验证需要 Sepolia ETH这意味着本教程所有npx hardhat test跑出的加密在默认模式下实际是插件提供的mock 加密值用于本地快速验证逻辑只有切换到 Sepolia 测试网才会使用真实加密的完整 FHEVM 栈。部署到测试网的完整流程npx hardhat deploy --network sepolia、npx hardhat fhevm check-fhevm-compatibility等同样记录在 docs/solidity-guides/hardhat/run_test.md。从源码层印证本文的核心概念FHECounter.sol的完整实现位于 docs/examples/fhe-counter.md合约继承ZamaEthereumConfig导入FHE, euint32, externalEuint32increment/decrement均通过FHE.fromExternal验证外部密文、以FHE.add/FHE.sub完成同态运算FHE 库本身定义在 library-solidity/lib/FHE.solfromExternal、add、sub、allow、allowThis等函数均在此实现是理解链上密文语义的第一手资料FHEVM 配置合约位于 library-solidity/config/ZamaConfig.solZamaEthereumConfig为其对以太坊主网与 Sepolia 测试网的配置适配Hardhat 插件 APIcreateEncryptedInput、userDecryptEuint、FhevmType的完整用法见 docs/solidity-guides/hardhat/write_test.md。恭喜你已经完成了整个教程你已成功编写并测试了基于 FHEVM 的计数器智能合约。至此你的项目应包含以下文件contracts/FHECounter.sol—— 你的 Solidity FHEVM 智能合约完整代码见 docs/examples/fhe-counter.md 的FHECounter.sol标签页test/FHECounter.ts—— 你的 TypeScript 编写的 Hardhat 测试套件完整代码见同一文件的FHECounter.ts标签页。回顾一下你在本文中掌握的四个核心测试能力验证加密句柄的未初始化状态通过ethers.ZeroHash断言euint32变量初始为bytes32(0)构造上下文绑定的加密输入createEncryptedInput(contractAddress, userAddress).add32(x).encrypt()得到handles与inputProof携带 ZKPoK 调用合约increment(encryptedOne.handles[0], encryptedOne.inputProof)保证密文无法跨用户、跨合约复用链下解密并断言明文userDecryptEuint(FhevmType.euint32, handle, contractAddress, signer)前提是合约与调用者均已通过FHE.allow获得权限。下一步如果想将项目部署到测试网或想了解更多 FHEVM Hardhat 插件的用法请阅读《Deploy contracts and run tests》。在此之前你还可以先阅读《Write a simple contract》与《Turn it into FHEVM》将本系列教程的前置合约与后续测试串成完整的开发链路。【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表