fhEVM 加密输入(Encrypted Inputs)完整指南:从 ZKPoK 验证到合约实战
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
fhEVM 的加密输入机制允许用户在客户端用 FHE 公钥加密敏感数据后提交上链,并以零知识证明(ZKPoK)保证密文真实性,让智能合约在不接触明文的前提下完成余额更新、权限判定等私密计算。本文以 docs/solidity-guides/inputs.md 为主线,结合仓库内library-solidity与host-contracts的源码实现,系统讲解加密输入的类型体系、Hardhat 端生成流程、FHE.fromExternal/FHE.toExternal的验证原理,以及合约到合约组合(contract-to-contract composition)的 ACL 权限细节。读完本文,你将能够为 Solidity 合约编写标准化的加密输入入口,并正确配套 JS/TS SDK 生成可被 fhEVM 验证的密文与证明。
什么是加密输入
加密输入(Encrypted Inputs)是用户在链下用 FHE 公钥加密后提交到链上的数据值。它让敏感信息保持机密的同时仍能被智能合约直接处理。每个加密输入都伴随一个ZKPoK(Zero-Knowledge Proof of Knowledge,知识零知识证明),用于在不泄露明文的前提下证明提交者确实知道该密文对应的明文,从而防止重放攻击与滥用。
核心特性
- 机密性(Confidentiality):数据使用公开的 FHE 公钥加密,只有被授权方才能解密或处理这些数值。加密发生在客户端(浏览器/DApp 前端),明文永远不上链。
- ZKPoK 验证(Validation via ZKPoKs):每个加密输入都附带一个证明,用于验证用户确实知道密文的明文值,防止重放攻击或恶意提交。
- 高效打包(Efficient packing):同一笔交易中的所有输入被打包进单个密文中,并按用户自定义顺序排列,从而优化零知识证明的体积与生成成本。这与 fhEVM 的 InputVerifier 合约所采用的“一个 ciphertext + 一个 proof + 多个 handles”模型相呼应。
加密函数中的参数类型
当合约函数需要接收加密输入时,会同时出现两类参数:
externalEbool、externalEaddress、externalEuintXX:指向证明内加密参数索引的句柄(handle),表示一个具体的加密输入引用。在 fhEVM 中这些类型本质上是bytes32的强类型包装(wrapper)。bytes:包含密文和用于验证的零知识证明(即inputProof)。
一个接收多个加密参数的 Solidity 函数示例:
function exampleFunction( externalEbool param1, externalEuint64 param2, externalEuint8 param3, bytes calldata inputProof ) public { // Function logic here }在这个例子中,param1、param2、param3分别是ebool、euint64、euint8的加密输入句柄,而inputProof包含验证它们真实性的对应 ZKPoK。
从源码生成角度看,这些外部类型由 library-solidity/codegen/src/templateFHEDotSol.ts 中的模板统一生成——第 103 行注释明确指出代码生成器负责“从明文与externalEXXX到所有受支持类型的句柄转换(如externalEbool --> ebool、uint32 --> euint32)”,因此合约中可用的外部类型与euint4~euint256、ebool、eaddress一一对应。底层类型枚举定义见 library-solidity/lib/FheType.sol,其中Uint160(对应eaddress)、Uint8~Uint256(对应各类euintXX)与Bool均为验证时使用的目标类型标识。
使用 Hardhat 生成加密输入
下面示例中,我们使用 Alice 的地址创建加密输入并提交交易:
import { fhevm } from "hardhat"; const input = fhevm.createEncryptedInput(contract.address, signers.alice.address); input.addBool(canTransfer); // at index 0 input.add64(transferAmount); // at index 1 input.add8(transferType); // at index 2 const encryptedInput = await input.encrypt(); const externalEboolParam1 = encryptedInput.handles[0]; const externalEuint64Param2 = encryptedInput.handles[1]; const externalEuint8Param3 = encryptedInput.handles[2]; const inputProof = encryptedInput.inputProof; tx = await myContract .connect(signers.alice) "exampleFunction(bytes32,bytes32,bytes32,bytes)" ; await tx.wait();关键点解读:
fhevm.createEncryptedInput(contract.address, signers.alice.address)第一个参数指定密文的目标合约,第二个参数指定密文的属主(owner)。属主会先被授予该句柄的使用权限(ACL),随后在链上验证时再通过allowTransient进行临时授权。addBool/add64/add8等 API 依次往打包密文中追加数值,其添加顺序即后续句柄在handles数组中的索引(index 0、1、2…)。encrypt()完成后,encryptedInput.handles是按添加顺序排列的句柄数组,encryptedInput.inputProof是对整批密文的 ZKPoK。- 注意 ABI 签名中外部类型都以
bytes32出现在接口签名里(如"exampleFunction(bytes32,bytes32,bytes32,bytes)"),这与外部类型本质是bytes32包装的实现一致。
输入顺序(Input Order)
开发者可以自由设计函数参数顺序,TypeScript 中构造加密输入的顺序与 Solidity 函数参数的顺序之间没有强制对应关系。每个句柄通过其在证明中的索引独立引用一个密文槽位,只要句柄与参数类型匹配(例如handles[1]是euint64,就传给externalEuint64形参),顺序完全可以按业务需要排列。
验证加密输入:FHE.fromExternal 与 FHE.asEuintXX
合约通过FHE.asEuintXX、FHE.asEbool、FHE.asEaddress等函数验证并转换加密输入。不过需要说明的是,asEuintXX系列还有另一层含义——在 library-solidity/lib/FHE.sol 中它们同时被重载为“类型转换”(如asEuint8(euint64))与“明文平凡加密”(如asEuint8(uint8),内部调用Impl.trivialEncrypt)。真正承担“验证外部输入”职责的入口是FHE.fromExternal(externalType, inputProof),其内部会调用Impl.verify完成密文校验与类型转换。
验证示例
function myExample(externalEuint64 encryptedAmount, externalEbool encryptedToggle, bytes calldata inputProof) public { // Validate and convert the encrypted inputs euint64 amount = FHE.fromExternal(encryptedAmount, inputProof); ebool toggleFlag = FHE.fromExternal(encryptedToggle, inputProof); // Update the user's encrypted balance balances[msg.sender] = FHE.add(balances[msg.sender], amount); // Toggle the user's encrypted flag userFlags[msg.sender] = FHE.not(toggleFlag); // FHE permissions and function logic here ... } // Function to retrieve a user's encrypted balance function getEncryptedBalance() public view returns (euint64) { return balances[msg.sender]; } // Function to retrieve a user's encrypted flag function getEncryptedFlag() public view returns (ebool) { return userFlags[msg.sender]; }ConfidentialERC20.sol 中的验证示例
仓库中的真实代币合约 library-solidity/examples/EncryptedERC20.sol 采用了完全相同的模式(第 76、109、145 行均声明了externalEuint64 encryptedAmount形参),其transfer骨架如下:
function transfer( address to, externalEuint64 encryptedAmount, bytes calldata inputProof ) public { // Verify the provided encrypted amount and convert it into an encrypted uint64 euint64 amount = FHE.fromExternal(encryptedAmount, inputProof); // Function logic here, such as transferring funds ... }验证如何工作
FHE.fromExternal的完整实现位于 library-solidity/lib/FHE.sol 第 8497~8778 行(对ebool、euint8~euint256、eaddress各有一个重载),其验证逻辑分两条路径:
- 输入验证(Input verification):当
inputProof.length != 0时,调用Impl.verify(inputHandle, inputProof, FheType)。从 library-solidity/lib/Impl.sol 第 755 行可以看到,Impl.verify会调用IFHEVMExecutor(CoprocessorAddress).verifyInput(inputHandle, msg.sender, inputProof, toType),随后调用IACL(ACLAddress).allowTransient(result, msg.sender)——即验证成功后立即将句柄对当前调用者做瞬态授权,保证本交易内后续 FHE 运算可正常使用。 - 类型转换(Type conversion):验证通过后,函数把
externalEbool、externalEaddress、externalEuintXX转换为对应的加密类型(ebool、eaddress、euintXX),供合约内继续参与FHE.add、FHE.not等运算。
在链上验证侧,verifyInput的底层实现在 host-contracts/contracts/InputVerifier.sol:它会做链 ID 校验(InvalidChainId)、证明格式反序列化(DeserializingInputProofFail)、句柄版本检查(InvalidHandleVersion)、句柄与密文槽位一致性检查(InvalidInputHandle)、阈值签名验证(SignatureThresholdNotReached)等,任一环节失败即回滚交易,从源码层面印证了“每个加密输入必须携带合法 ZKPoK”这一安全约束。
用 FHE.toExternal 再导出句柄:合约到合约组合
FHE.toExternal是FHE.fromExternal的逆操作:它把合约已经持有的链上加密值(euintXX、ebool、eaddress)重新包装为对应的外部类型(externalEuintXX、externalEbool、externalEaddress)。该函数被重载,目标类型由参数类型推断——与fromExternal完全对称。
为什么需要 toExternal
许多标准接口(包括机密代币标准接口)的入口类型是(externalEuintXX handle, bytes inputProof),因为常规调用者是提交“加密输入 + ZKPoK”的 EOA。而一个合约已经持有euintXX,在链上无法再生成零知识证明。toExternal配合fromExternal的空证明路径正好弥合这一缺口:
// Inside a contract that already holds `euint64 amount`: FHE.allow(amount, address(token)); // grant the callee access (ACL) token.transfer(to, FHE.toExternal(amount), ""); // re-wrap, pass an empty proof在接收方,FHE.fromExternal(encryptedAmount, "")会跳过证明验证,仅检查句柄是否已被授权给调用者,然后作为普通euint64返回。这正是智能合约账户(smart contract accounts)接入 fhEVM 输入 ABI 的机制。
空证明路径的源码细节
从 library-solidity/lib/FHE.sol 的实现看,fromExternal空证明路径的逻辑是:
- 若句柄为
bytes32(0),直接返回平凡加密的零值(asEbool(false)/asEuintXX(0)/asEaddress(address(0))); - 否则调用
Impl.isAllowed(inputBytes32, msg.sender)检查 ACL,未授权则抛出SenderNotAllowedToUseHandle错误(见 library-solidity/lib/Impl.sol 第 872 行,其内部调用IACL(...).isAllowed(handle, account))。
而toExternal的实现只是把内部句柄重新wrap成外部类型(如externalEuint64.wrap(euint64.unwrap(value))),不做任何验证、不授予任何权限。与之配套的FHE.allow(handle, account)在Impl.allow中调用IACL(...).allow(handle, account)持久化授权(library-solidity/lib/Impl.sol 第 828 行)。
重要提示:
toExternal只改变静态类型——它不执行验证,也不授予访问权限。空证明重新导入时,除非句柄已通过FHE.allow/FHE.allowThis授权给相关方,否则会以SenderNotAllowedToUseHandle回滚。务必先设置 ACL 权限。
最佳实践
- 输入打包(Input packing):把同一笔交易的所有加密输入打包进单个密文,可最小化零知识证明的体积与复杂度,降低 gas 成本并加快链上验证(对应
createEncryptedInput的add*系列 API)。 - 前端加密(Frontend encryption):始终在客户端使用 FHE 公钥加密输入,确保数据在上链前即为密文,明文永不离开用户设备。
- 证明管理(Proof management):确保每个加密输入关联正确的零知识证明,避免验证失败。特别注意句柄索引与参数类型的匹配,以及
inputProof必须与整批密文对应。 - 合约组合时先授权(ACL first):使用
FHE.toExternal进行合约间调用时,先在发送方合约执行FHE.allow(handle, callee)(或FHE.allowThis),再传入空证明,否则接收方fromExternal会因SenderNotAllowedToUseHandle回滚。
加密输入及其验证构成了 fhEVM 中安全、私密交互的基石。掌握externalEXXX类型、FHE.fromExternal/FHE.toExternal的完整语义,并正确搭配 SDK 侧的createEncryptedInput与 ACL 授权,开发者就能构建既保护数据机密性、又不牺牲功能与可扩展性的隐私保护智能合约。
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考