news 2026/9/12 6:35:21

fhEVM 多值用户解密实战:用 UserDecryptMultipleValues 在 Hardhat 中安全解密 ebool / euint32 / euint64

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
fhEVM 多值用户解密实战:用 UserDecryptMultipleValues 在 Hardhat 中安全解密 ebool / euint32 / euint64

fhEVM 多值用户解密实战:用 UserDecryptMultipleValues 在 Hardhat 中安全解密 ebool / euint32 / euint64

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

本文以 fhEVM 开源仓库中 docs/examples/fhe-user-decrypt-multiple-values.md 为核心,讲解如何在一条链上一次用户解密多个密文句柄(ebool、euint32、euint64),涵盖链上 FHE 权限授权、EIP-712 签名、relayer-sdk 的userDecrypt调用与测试断言。读完本文,你将掌握"链上授权 + 链下解密"的完整调用链,并能直接复用仓库中的示例合约与 Hardhat 测试代码。

什么是用户解密(User Decryption)

用户解密是 fhEVM 提供的一种按用户粒度的解密机制:它允许特定用户解密某个加密值,同时对该值对其他用户保持隐藏。与公共解密(public decryption,解密后全网可见)不同,用户解密只在持有正确 FHE 权限的授权用户侧还原明文。

用户解密的关键特性是:

  • 权限在链上授予:通过智能合约中的FHE.allow(ciphertext, address)等函数写入访问控制(ACL);
  • 解密在链下发生:实际的重加密/解密请求由**前端应用(前端应用)**在链下发起,借助 Relayer 与 KMS(密钥管理系统)完成;
  • 明文永不上链:数据始终保持在区块链 FHE 密钥下的密文状态,但可以被安全地重加密到用户自己的 NaCl 公钥之下,只有该用户能解开。

从仓库的 docs/sdk-guides/user-decryption.md 可以看出,用户解密(也常被称为 re-encrypt,重加密)还支撑着"加密数据在不同合约、dApp 或用户之间安全传递与复用"的场景。其整体流程分为两步:

  1. 取回密文:调用合约的 view 函数,从链上取回密文句柄(ciphertext handle);
  2. 客户端解密:在客户端使用@zama-fhe/relayer-sdk对句柄执行用户解密,将密文重加密到用户公钥下。

前置准备:示例文件的目录结构

官方文档强调,运行本示例前必须把文件放到正确的位置,否则 Hardhat 无法编译与测试:

  • .sol文件 →<your-project-root-dir>/contracts/
  • .ts文件 →<your-project-root-dir>/test/

UserDecryptMultipleValues.sol放在项目的contracts/目录,UserDecryptMultipleValues.ts放在test/目录。仓库中对应的完整 Hardhat 测试工程可参考 test-suite/e2e/contracts 与 test-suite/e2e/test 的布局,以及 docs/solidity-guides/hardhat 下的环境搭建指南。

另外,运行本例还需要以下依赖:

  • @fhevm/solidity(FHE.sol、ZamaConfig.sol 所在库,仓库中对应 library-solidity/lib/FHE.sol);
  • @fhevm/hardhat-plugin(提供hre.fhevm运行时环境);
  • @fhevm/mock-utils(提供timestampNow等工具);
  • @zama-fhe/relayer-sdk(提供DecryptedResults类型与真正的用户解密能力,仓库中对应 sdk/js-sdk/src/core/modules/relayer);
  • hardhatchai@nomicfoundation/hardhat-ethers等常规测试栈。

第一步:编写合约,链上授予 FHE 权限

示例合约继承自ZamaEthereumConfig(位于 library-solidity/config/ZamaConfig.sol),它负责注入当前网络的 FHE 相关合约地址配置。完整合约代码如下(与仓库文档一致):

// SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, ebool, euint32, euint64 } from "@fhevm/solidity/lib/FHE.sol"; import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; contract UserDecryptMultipleValues is ZamaEthereumConfig { ebool private _encryptedBool; // = 0 (uninitizalized) euint32 private _encryptedUint32; // = 0 (uninitizalized) euint64 private _encryptedUint64; // = 0 (uninitizalized) // solhint-disable-next-line no-empty-blocks constructor() {} function initialize(bool a, uint32 b, uint64 c) external { // Compute 3 trivial FHE formulas // _encryptedBool = a ^ false _encryptedBool = FHE.xor(FHE.asEbool(a), FHE.asEbool(false)); // _encryptedUint32 = b + 1 _encryptedUint32 = FHE.add(FHE.asEuint32(b), FHE.asEuint32(1)); // _encryptedUint64 = c + 1 _encryptedUint64 = FHE.add(FHE.asEuint64(c), FHE.asEuint64(1)); // see `DecryptSingleValue.sol` for more detailed explanations // about FHE permissions and asynchronous user decryption requests. FHE.allowThis(_encryptedBool); FHE.allowThis(_encryptedUint32); FHE.allowThis(_encryptedUint64); FHE.allow(_encryptedBool, msg.sender); FHE.allow(_encryptedUint32, msg.sender); FHE.allow(_encryptedUint64, msg.sender); } function encryptedBool() public view returns (ebool) { return _encryptedBool; } function encryptedUint32() public view returns (euint32) { return _encryptedUint32; } function encryptedUint64() public view returns (euint64) { return _encryptedUint64; } }

合约要点:

  • initialize(bool a, uint32 b, uint64 c)通过FHE.asEbool / asEuint32 / asEuint64把明文包装为 trivial 密文,再分别计算三个"平凡 FHE 公式":a ^ falseb + 1c + 1,最终得到ebooleuint32euint64三种不同类型的加密值。
  • 三个encrypted*()view 函数用于向外部暴露密文句柄,测试端正是通过这些句柄发起用户解密的。
  • 每一笔密文都同时调用了FHE.allowThis(...)FHE.allow(..., msg.sender),这是用户解密能够成功的关键前提。

为什么allowThisallow缺一不可

从仓库源码 library-solidity/lib/FHE.sol 可以看到allowallowThis的底层实现都转发到Impl.allow(bytes32 handle, address account)(见 library-solidity/lib/Impl.sol),差别仅在于授权对象:

  • FHE.allowThis(value)等价于Impl.allow(handle, address(this)),把权限授予合约自身
  • FHE.allow(value, account)把权限授予指定账户(这里即调用者msg.sender)。

配套的单值示例 docs/examples/fhe-user-decrypt-single-value.md 明确指出了常见的坑:如果只调用FHE.allow(value, msg.sender)而忘记FHE.allowThis(value),用户解密必然失败。原因在于用户解密请求需要合约本身具备对该句柄的权限(合约是持有密文的实体),同时用户也需要被授权。对应地,其测试用例断言会以"dapp contract (.+) is not authorized to user decrypt handle (.+)."的形式被拒绝。因此,多值示例对三个密文逐一执行allowThis+allow是"能跑通"的最低安全配置。

关于 ACL 权限模型的更多细节(如授权、撤销、过期时间、委托),可继续阅读 docs/solidity-guides/acl/README.md 与 docs/solidity-guides/acl/acl_examples.md。

第二步:编写 Hardhat 测试,链下批量解密

完整测试代码如下(与仓库文档一致):

import { UserDecryptMultipleValues, UserDecryptMultipleValues__factory } from "../../../types"; import type { Signers } from "../../types"; import { HardhatFhevmRuntimeEnvironment } from "@fhevm/hardhat-plugin"; import { utils as fhevm_utils } from "@fhevm/mock-utils"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { DecryptedResults } from "@zama-fhe/relayer-sdk"; import { expect } from "chai"; import { ethers } from "hardhat"; import * as hre from "hardhat"; async function deployFixture() { // Contracts are deployed using the first signer/account by default const factory = (await ethers.getContractFactory("UserDecryptMultipleValues")) as UserDecryptMultipleValues__factory; const userDecryptMultipleValues = (await factory.deploy()) as UserDecryptMultipleValues; const userDecryptMultipleValues_address = await userDecryptMultipleValues.getAddress(); return { userDecryptMultipleValues, userDecryptMultipleValues_address }; } /** * This trivial example demonstrates the FHE user decryption mechanism * and highlights a common pitfall developers may encounter. */ describe("UserDecryptMultipleValues", function () { let contract: UserDecryptMultipleValues; let contractAddress: string; let signers: Signers; before(async function () { // Check whether the tests are running against an FHEVM mock environment if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { owner: ethSigners[0], alice: ethSigners[1] }; }); beforeEach(async function () { // Deploy a new contract each time we run a new test const deployment = await deployFixture(); contractAddress = deployment.userDecryptMultipleValues_address; contract = deployment.userDecryptMultipleValues; }); // ✅ Test should succeed it("user decryption should succeed", async function () { const tx = await contract.connect(signers.alice).initialize(true, 123456, 78901234567); await tx.wait(); const encryptedBool = await contract.encryptedBool(); const encryptedUint32 = await contract.encryptedUint32(); const encryptedUint64 = await contract.encryptedUint64(); // The FHEVM Hardhat plugin provides a set of convenient helper functions // that make it easy to perform FHEVM operations within your Hardhat environment. const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; const aliceKeypair = fhevm.generateKeypair(); const startTimestamp = fhevm_utils.timestampNow(); const durationDays = 365; const aliceEip712 = fhevm.createEIP712(aliceKeypair.publicKey, [contractAddress], startTimestamp, durationDays); const aliceSignature = await signers.alice.signTypedData( aliceEip712.domain, { UserDecryptRequestVerification: aliceEip712.types.UserDecryptRequestVerification }, aliceEip712.message, ); const decrytepResults: DecryptedResults = await fhevm.userDecrypt( [ { handle: encryptedBool, contractAddress: contractAddress }, { handle: encryptedUint32, contractAddress: contractAddress }, { handle: encryptedUint64, contractAddress: contractAddress }, ], aliceKeypair.privateKey, aliceKeypair.publicKey, aliceSignature, [contractAddress], signers.alice.address, startTimestamp, durationDays, ); expect(decrytepResults[encryptedBool]).to.equal(true); expect(decrytepResults[encryptedUint32]).to.equal(123456 + 1); expect(decrytepResults[encryptedUint64]).to.equal(78901234567 + 1); }); });

测试执行链路可拆解为六个环节:

  1. 环境校验hre.fhevm.isMock为真才继续运行。该示例是面向本地 mock 环境(Hardhat)的测试,不能直接在 Sepolia 测试网运行——mock 环境由@fhevm/hardhat-plugin提供本地 FHE 模拟能力。
  2. 部署与取句柄:每个用例重新部署合约,initialize(true, 123456, 78901234567)alice签名调用(contract.connect(signers.alice)),随后通过三个 view 函数拿到三个密文句柄。
  3. 生成密钥对fhevm.generateKeypair()为 alice 生成 NaCl 密钥对,用户的公钥将作为重加密的目标公钥。
  4. 构造并签署 EIP-712 请求fhevm.createEIP712(publicKey, [contractAddress], startTimestamp, durationDays)生成类型化数据,其中durationDays = 365表示该授权有效期为 365 天;随后用 alice 的以太坊私钥对UserDecryptRequestVerification类型消息签名。
  5. 批量用户解密fhevm.userDecrypt(handleContractPairs, privateKey, publicKey, signature, contractAddresses, userAddress, startTimestamp, durationDays)一次传入三个{ handle, contractAddress }对,一次性返回所有解密结果。
  6. 断言明文decrytepResults以句柄为键,校验true123456 + 178901234567 + 1三个明文,证明三种类型(ebool / euint32 / euint64)均被正确解密。

userDecrypt 的参数语义

对照 docs/sdk-guides/user-decryption.md 中的前端示例,userDecrypt的参数含义如下:

参数含义
handleContractPairs{ handle, contractAddress }[]数组,声明要解密的句柄及各自所属合约
privateKey / publicKey用户生成的 NaCl 密钥对(公钥用于重加密,私钥用于本地解开)
signature用户对 EIP-712 解密请求的签名(在真实环境需要去掉0x前缀)
contractAddresses本次请求授权的合约地址列表,用于在请求签名中声明权限范围
userAddress用户的以太坊地址
startTimestamp授权起始时间戳(秒)
durationDays授权有效期(天),与签名共同构成时间窗口约束

在非 Hardhat 的真实前端环境中,签名流程为:先用instance.createEIP712(publicKey, contractAddresses, startTimeStamp, durationDays)构建类型化数据,再用signer.signTypedData签名,最后调用instance.userDecrypt(...),其中签名需要signature.replace("0x", "")去掉前缀。而 Hardhat 插件版的fhevm.userDecrypt封装了同样的流程,使测试代码更简洁。

第三步:用户解密在底层发生了什么

在 Hardhat mock 环境中,插件直接本地完成解密;而在真实网络上,userDecrypt走的是 Relayer + KMS 通道。仓库 sdk/js-sdk/src/core/modules/relayer/cleartext/fetchUserDecryptV1.ts 展示了关键实现,可以归纳出以下底层流程:

  1. 授权校验:遍历所有handleContractPairs,逐一检查每个合约地址是否出现在 EIP-712 请求的contractAddresses列表中,不在列表中直接抛出ContractAddressNotAuthorized错误(见fetchUserDecryptV1开头部分)。
  2. 链上/链下分支:通过isForgeFhevmV1判断当前是否处于 Forge mock(链下)环境;链下环境调用runUserDecryptOffChain直接读取明文,否则调用runUserDecryptOnChain
  3. 链上路径runUserDecryptOnChain调用 KMS Verifier 合约的userDecryptview 函数(ABI 见该文件的userDecryptAbi,入参正是pairsuserAddresspublicKeycontractAddressesstartTimestampdurationDaysuserSignature),返回统一的重加密载荷、KMS signer 地址列表、阈值thresholdextraData。也就是说,链上的 ACL 检查发生在 KMS Verifier 合约内部。
  4. 阈值签名:从当前 KMS Signers 上下文中读取 signer 与阈值,随机选取满足阈值数量的 signer,用其私钥对"公共载荷"签名,产出多份签名分片(KmsSigncryptedShares)。
  5. 本地解开:SDK 侧使用用户私钥对收到的分片进行解密/组合,最终得到明文,并按键(句柄)组织成DecryptedResults返回给调用方。

这解释了为何链上必须通过FHE.allowThisFHE.allow同时授权:KMS Verifier 的userDecrypt只有在合约与用户都具备该句柄的 ACL 权限时才会返回可用的重加密载荷

与 Relayer 前端 SDK 的对应关系

在真实 dApp 中,链下部分由@zama-fhe/relayer-sdkcreateInstance初始化(见 docs/sdk-guides/initialization.md),实例需要配置 ACL 合约地址、KMS Verifier 合约地址、Input Verifier 地址、网关链上的解密验证地址、host 链与网关链的 chainId、RPC 与 Relayer URL。初始化完成后,即可用与测试完全一致的模式执行generateKeypair → createEIP712 → signTypedData → userDecrypt拿到明文。

常见坑与注意事项

结合官方文档与仓库代码,多值用户解密最常见的三个问题:

  1. 忘记FHE.allowThis:只给用户授权而不给合约自身授权,用户解密会被 ACL 拒绝。单值示例专门用initializeUint32Wrong演示了这个失败路径(见 docs/examples/fhe-user-decrypt-single-value.md)。
  2. 句柄与合约地址不匹配userDecryptpairs中每个句柄都必须标注其真实所属合约地址;同时contractAddresses数组必须覆盖所有涉及的合约,否则 SDK 会在本地直接抛出ContractAddressNotAuthorized
  3. mock 与真实网络的差异:本例的 Hardhat 测试依赖hre.fhevm.isMock为真,只能跑在本地 mock 环境;要对接 Sepolia 测试网,应改用@zama-fhe/relayer-sdkcreateInstance+instance.userDecrypt流程,且签名需去除0x前缀、时间戳与durationDays需要与签名严格一致。

扩展阅读

  • 单值用户解密示例:docs/examples/fhe-user-decrypt-single-value.md
  • 用户解密完整协议说明:docs/sdk-guides/user-decryption.md
  • Relayer SDK 初始化:docs/sdk-guides/initialization.md
  • ACL 权限模型:docs/solidity-guides/acl/README.md 与 docs/solidity-guides/acl/acl_examples.md
  • FHE.allow / allowThis实现:library-solidity/lib/FHE.sol
  • Relayer 用户解密底层实现:sdk/js-sdk/src/core/modules/relayer/cleartext/fetchUserDecryptV1.ts

【免费下载链接】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 6:34:52

Prompt as Code:工业级提示工程实践指南

我无法根据当前输入生成符合要求的博文。 原因如下&#xff1a; 项目标题 "awesome-gpt-image-2" 缺乏明确的技术指向、功能定义或领域背景&#xff0c;仅是一个命名风格&#xff08;类似开源项目命名惯例&#xff09;&#xff0c;但未说明其本质是工具库&#xff…

作者头像 李华
网站建设 2026/9/12 6:33:37

Linux离线开发环境部署实战与解决方案

1. 项目概述&#xff1a;Linux开发环境离线安装的核心挑战在嵌入式开发和工业自动化领域&#xff0c;经常遇到需要在内网或隔离环境中搭建完整开发环境的情况。传统在线安装方式依赖网络仓库&#xff0c;而HoRain云方案提供了一套完整的离线部署方法论。我最近在国产化替代项目…

作者头像 李华
网站建设 2026/9/12 6:31:32

具身智能数据采集实验方案:标准化流程与量化测评实践

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

作者头像 李华
网站建设 2026/9/12 6:28:41

Jupyter Notebook 7 Tab缩进失效?5步自查到修复的完整排障路径

Jupyter Notebook 7 Tab缩进失效&#xff1f;5步自查到修复的完整排障路径 【免费下载链接】notebook Jupyter Interactive Notebook 项目地址: https://gitcode.com/GitHub_Trending/no/notebook 在 Jupyter Notebook 7 的代码单元格里按 Tab 没反应&#xff0c;手动敲…

作者头像 李华
网站建设 2026/9/12 6:26:26

Python闭包与装饰器:原理、应用与性能优化

1. Python闭包与装饰器&#xff1a;从入门到精通在Python开发中&#xff0c;闭包和装饰器是两个既基础又强大的概念。很多初学者第一次接触时都会感到困惑&#xff0c;但一旦掌握&#xff0c;它们能大幅提升代码的简洁性和可维护性。我在实际项目中多次使用这两种技术解决复杂问…

作者头像 李华