ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

使用 fhEVM 构建密封式 NFT 盲拍:全同态加密链上保密拍卖实战指南

2026/9/12 13:00:53 拓冰建站 浏览量
使用 fhEVM 构建密封式 NFT 盲拍:全同态加密链上保密拍卖实战指南 使用 fhEVM 构建密封式 NFT 盲拍全同态加密链上保密拍卖实战指南【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm密封式拍卖Sealed-Bid Auction要求所有参与者在拍卖结束前互相隐瞒出价而 fhEVM 通过全同态加密FHE让智能合约可以直接对密文出价进行安全比较全程不泄露任何一方的出价金额仅在结算时揭示赢家。本文以BlindAuction.sol为例完整讲解如何在 FHEVM 上实现提交加密出价、密文比较、公开解密揭示赢家、领奖与退款的整套保密拍卖流程并结合本仓库源码FHE.sol、ZamaConfig.sol剖析每个 FHE 原语fromExternal、allow、select、makePubliclyDecryptable、checkSignatures等的底层行为。读完本文你将掌握加密出价的接收与校验、无需解密即可完成的出价比较、基于公开解密与 KMS 证明的赢家揭示以及一套可直接运行的 Hardhat 测试流程。为什么拍卖需要 FHE在大多数链上拍卖中出价是完全公开的。任何人既可以扫描链上历史数据也可以监控内存池中待确认的交易从而知道每位参与者出价多少。这直接破坏了拍卖的公平性——赢家只需要比当前最高出价多出 1 wei 即可出价行为退化为盯梢竞拍。现有的 commit-reveal提交-揭示方案试图在提交阶段隐藏出价但它有几大缺点额外的交易开销需要提交与揭示两个阶段的多次交易糟糕的用户体验例如要求用户通过CREATE2预先向 EOA 转入资金多阶段延迟完整的拍卖被拆成若干阶段流程冗长。FHE 则允许参与者一步完成加密出价出价以密文形式直接提交给智能合约合约在密文上完成比较与状态更新全程无需解密也没有多阶段复杂度既保住了出价机密性又显著改善了用户体验。这正是 fhEVM 相对传统方案的核心理由。项目准备开始之前需要完成以下三步准备安装 FHEVM Hardhat 模板配置 OpenZeppelin confidential contracts机密合约库部署一个机密代币Confidential Token。环境要求Node.js 20、Hardhat ^2.24、可访问 FHEVM 网络与 Zama 网关/中继器与具体的依赖安装命令可参考 设置 OpenZeppelin confidential contracts 教程机密代币的完整实现与测试含ERC7984Example、机密铸造/销毁、总供应量可见性等扩展见 部署 Confidential Token 教程。在本文的拍卖中支付手段选用的是机密 ERC7984 代币而不是普通 ERC20。原因在于即使拍卖合约的内部状态是加密的任何人仍可通过监控代币转账交易来猜测出价金额而 ERC7984 让余额与转账金额全程保持密文。任何 ERC20 都可以通过 ERC7984ERC20Wrapper 包装成 ERC7984 来隐藏后续转账。合约继承的ZamaEthereumConfig提供了与 Zama Protocol 交互所需的网络参数。从源码看ZamaConfig.solgetCoprocessorConfig()会按block.chainid自动路由到对应网络的 ACL、Coprocessor 与 KMSVerifier 地址网络chainIdEthereum 主网1Polygon 主网137Ethereum Sepolia 测试网11155111Polygon Amoy 测试网80002本地 Hardhat/Anvil31337在其他链上部署会直接revert ZamaProtocolUnsupported()。创建拍卖合约在./contracts/目录新建BlindAuction.sol。为了启用 FHE 操作合约需要继承ZamaEthereumConfig同时继承ReentrancyGuard防重入与IERC721Receiver接收作为奖品的 NFT// SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.27; import { FHE, externalEuint64, euint64, eaddress, ebool } from fhevm/solidity/lib/FHE.sol; import { ZamaEthereumConfig } from fhevm/solidity/config/ZamaConfig.sol; import { IERC721 } from openzeppelin/contracts/token/ERC721/IERC721.sol; import { IERC721Receiver } from openzeppelin/contracts/token/ERC721/IERC721Receiver.sol; import { ReentrancyGuard } from openzeppelin/contracts/utils/ReentrancyGuard.sol; import { IERC7984 } from openzeppelin/confidential-contracts/interfaces/IERC7984.sol; contract BlindAuction is ZamaEthereumConfig, ReentrancyGuard, IERC721Receiver { /// notice The recipient of the highest bid once the auction ends address public beneficiary; /// notice Confidential payment token (ERC7984) IERC7984 public confidentialToken; /// notice NFT prize for the auction IERC721 public nftContract; uint256 public tokenId; /// notice Auction duration uint256 public auctionStartTime; uint256 public auctionEndTime; constructor( address _nftContractAddress, address _confidentialTokenAddress, uint256 _tokenId, uint256 _auctionStartTime, uint256 _auctionEndTime ) { beneficiary msg.sender; confidentialToken IERC7984(_confidentialTokenAddress); nftContract IERC721(_nftContractAddress); tokenId _tokenId; // Transfer the NFT to the contract for the auction nftContract.safeTransferFrom(msg.sender, address(this), _tokenId); require(_auctionStartTime _auctionEndTime, INVALID_TIME); auctionStartTime _auctionStartTime; auctionEndTime _auctionEndTime; } }构造函数完成三件事记录受益人部署者、登记机密支付代币与 NFT 奖品、并把 NFT 通过safeTransferFrom托管到合约中因此合约必须实现onERC721Received以接收 ERC721 转账。用加密状态保存最高出价与赢家为了私密地保存最高出价与潜在赢家使用 FHE 库提供的加密类型eaddress存储加密地址euint64存储最高出价金额再用一个 mapping 记录每个出价者自己的出价/// notice Encrypted auction state euint64 private highestBid; eaddress private winningAddress; /// notice Mapping from bidder to their bid value mapping(address account euint64 bidAmount) private bids;关于euint64的性能提示与标准 Solidity 中uint64与uint256差别不大不同在 FHE 中数据的位宽对性能有显著影响——表示越大计算越昂贵。因此应基于实际场景谨慎选择表示位宽。本案例中euint64足以处理代币余额是够用且省的选择。除了上述加密状态完整示例见 sealed-bid-auction.md还补充了错误定义、事件、时间修饰器与查询视图这些构成了拍卖流程的骨架error TooEarlyError(uint256 time); error TooLateError(uint256 time); error WinnerNotYetRevealed(); event AuctionDecryptionRequested(eaddress encryptedWinningAddress); modifier onlyDuringAuction() { if (block.timestamp auctionStartTime) revert TooEarlyError(auctionStartTime); if (block.timestamp auctionEndTime) revert TooLateError(auctionEndTime); _; } modifier onlyAfterEnd() { if (block.timestamp auctionEndTime) revert TooEarlyError(auctionEndTime); _; } modifier onlyAfterWinnerRevealed() { if (winnerAddress address(0)) revert WinnerNotYetRevealed(); _; } function getEncryptedBid(address account) external view returns (euint64) { return bids[account]; } function getEncryptedWinningAddress() external view returns (eaddress) { return winningAddress; } function getWinnerAddress() external view returns (address) { require(winnerAddress ! address(0), Winning address has not been decided yet); return winnerAddress; }实现出价函数出价函数接收两个参数用户本地用 FHE 加密后的出价金额encryptedAmountexternalEuint64以及证明密文合法性的零知识证明inputProof。通过FHE.fromExternal()校验并拿到加密金额的引用function bid( externalEuint64 encryptedAmount, bytes calldata inputProof ) public onlyDuringAuction nonReentrant { // Get and verify the amount from the user euint64 amount FHE.fromExternal(encryptedAmount, inputProof); // ... }从源码看FHE.solfromExternal有两种路径当inputProof非空时调用Impl.verify()验证密文与证明当 proof 为空时若句柄为 0 则视为金额 0否则要求调用者已被allow否则抛出SenderNotAllowedToUseHandle。也就是说出价金额在进入合约逻辑前已经被严格校验。用余额差计算实际到账金额而非信任用户输入接下来把机密代币转入合约。这里有一个关键设计不能直接信任用户传入的amount。如果用户余额不足confidentialTransferFrom()不会回滚而是静默转移 0 值。这个设计刻意避免回滚——回滚交易会无意中泄露关于数据的某些信息例如这笔钱不够本身就是一种信号。euint64 balanceBefore confidentialToken.confidentialBalanceOf(address(this)); FHE.allowTransient(amount, address(confidentialToken)); confidentialToken.confidentialTransferFrom(msg.sender, address(this), amount); euint64 balanceAfter confidentialToken.confidentialBalanceOf(address(this)); euint64 sentBalance FHE.sub(balanceAfter, balanceBefore);因此合约通过转账前后合约余额的密文差FHE.sub(balanceAfter, balanceBefore)来推导实际到账金额彻底规避对用户输入的信任。FHE 工作方式说明链上执行的每个 FHE 操作都会发出一个事件用于构建计算图computation graph该图随后由 Zama Protocol 执行。也就是说FHE 运算并非在智能合约侧直接完成而是遵循合约生成的源图在链下/协处理器侧完成计算最终结果再回到链上。支持累计加价的出价记录用户可以追加自己的出价。若bids[msg.sender]已初始化则用FHE.add(previousBid, sentBalance)累加否则记录为首次出价euint64 previousBid bids[msg.sender]; if (FHE.isInitialized(previousBid)) { // The user increase his bid euint64 newBid FHE.add(previousBid, sentBalance); bids[msg.sender] newBid; } else { // First bid for the user bids[msg.sender] sentBalance; }FHE.isInitializedFHE.sol本质上检查加密句柄是否为 0用来区分尚未出价与已有出价两种状态。密文比较与分支选择最后判断是否需要更新加密赢家。核心是FHE.lt()小于比较返回密文布尔值与FHE.select()按密文条件二选一// Compare the total value of the user from the highest bid euint64 currentBid bids[msg.sender]; FHE.allowThis(currentBid); FHE.allow(currentBid, msg.sender); if (FHE.isInitialized(highestBid)) { ebool isNewWinner FHE.lt(highestBid, currentBid); highestBid FHE.select(isNewWinner, currentBid, highestBid); winningAddress FHE.select(isNewWinner, FHE.asEaddress(msg.sender), winningAddress); } else { highestBid currentBid; winningAddress FHE.asEaddress(msg.sender); } FHE.allowThis(highestBid); FHE.allowThis(winningAddress);这里涉及两个必须理解的概念访问控制FHE.allow/FHE.allowThis/FHE.allowTransient每个加密值都带有一个谁能读取的访问限制。要对某个密文做读取或计算必须显式申请访问权。本例中FHE.allowThis(currentBid)/FHE.allow(currentBid, msg.sender)让合约自身与出价者都能访问其出价值FHE.allowThis(highestBid)/FHE.allowThis(winningAddress)最高出价与赢家地址只允许合约自身访问直到结算时被公开解密。从源码看FHE.solallow/allowThis调用Impl.allow()持久授权而allowTransient调用Impl.allowTransient()Impl.sol仅在本交易内授权——后者正是把密文句柄传给代币合约做转账时的标准做法避免留下持久权限。分支branching /FHE.select如前所述FHE 场景下应避免交易回滚。构建 FHE 计算图时我们希望根据一个密文值走出两条路径。FHE.select(control, a, b)FHE.sol在密文条件下选择a或b若当前出价高于历史最高价则更新金额与地址否则保留旧值。这种分支方法尤其重要——链上无法直接读取密文但业务逻辑必须依据密文自适应。FHE.asEaddress(msg.sender)FHE.sol则是把明文地址平凡加密Impl.trivialEncryptImpl.sol为加密地址类型。完整的 bid 函数function bid(externalEuint64 encryptedAmount, bytes calldata inputProof) public onlyDuringAuction nonReentrant { // Get and verify the amount from the user euint64 amount FHE.fromExternal(encryptedAmount, inputProof); // Transfer the confidential token as payment euint64 balanceBefore confidentialToken.confidentialBalanceOf(address(this)); FHE.allowTransient(amount, address(confidentialToken)); confidentialToken.confidentialTransferFrom(msg.sender, address(this), amount); euint64 balanceAfter confidentialToken.confidentialBalanceOf(address(this)); euint64 sentBalance FHE.sub(balanceAfter, balanceBefore); // Update the bid balance (supports incremental bids) euint64 previousBid bids[msg.sender]; if (FHE.isInitialized(previousBid)) { euint64 newBid FHE.add(previousBid, sentBalance); bids[msg.sender] newBid; } else { bids[msg.sender] sentBalance; } // Compare the total value of the user against the highest bid euint64 currentBid bids[msg.sender]; FHE.allowThis(currentBid); FHE.allow(currentBid, msg.sender); if (FHE.isInitialized(highestBid)) { ebool isNewWinner FHE.lt(highestBid, currentBid); highestBid FHE.select(isNewWinner, currentBid, highestBid); winningAddress FHE.select(isNewWinner, FHE.asEaddress(msg.sender), winningAddress); } else { highestBid currentBid; winningAddress FHE.asEaddress(msg.sender); } FHE.allowThis(highestBid); FHE.allowThis(winningAddress); }结算阶段公开解密揭示赢家所有参与者出价完毕后进入结算阶段需要把加密的赢家地址解密出来。这里采用**公开解密public decryption**的两步流程先标记该值为可公开解密再在链上验证解密证明。第一步请求解密function decryptWinningAddress() public onlyAfterEnd { require(!decryptionRequested, Decryption already requested); decryptionRequested true; FHE.makePubliclyDecryptable(winningAddress); emit AuctionDecryptionRequested(winningAddress); }FHE.makePubliclyDecryptable()FHE.sol将加密的赢家地址标记为可公开解密随后发出的事件携带加密句柄供链下服务如 Zama Relayer计算解密并生成证明。关于该 API 与验证完整工作流可参考 公开解密 SDK 指南。注意该函数被onlyAfterEnd限制——拍卖进行中绝不能调用否则会泄露信息把当前领先者提前暴露给所有人。第二步提交解密结果并链上验证链下解密完成后任何人都可以提交结果与证明在链上完成验证function resolveAuction(bytes memory abiEncodedClearResult, bytes memory decryptionProof) public { require(decryptionRequested, Decryption not requested); require(winnerAddress address(0), Winner already resolved); bytes32[] memory cts new bytes32[](1); cts[0] FHE.toBytes32(winningAddress); FHE.checkSignatures(cts, abiEncodedClearResult, decryptionProof); address resultWinnerAddress abi.decode(abiEncodedClearResult, (address)); winnerAddress resultWinnerAddress; }abiEncodedClearResultABI 编码后的明文赢家地址decryptionProofKMS 签名证明验证解密结果的真实性FHE.toBytes32()FHE.sol把加密地址句柄转为bytes32列表FHE.checkSignatures()FHE.sol验证所提供的明文值确实是存储密文的真实解密结果——证明无效则交易回滚。双重require保证解密必须已被请求且赢家尚未被解析防止重复提交覆盖结果。赢家领奖与败者退款赢家被揭示后赢家可以领取奖品其余人可退回资金。赢家领取 NFTfunction winnerClaimPrize() public onlyAfterWinnerRevealed { require(winnerAddress msg.sender, Only winner can claim item); require(!isNftClaimed, NFT has already been claimed); isNftClaimed true; // Reset bid value bids[msg.sender] FHE.asEuint64(0); FHE.allowThis(bids[msg.sender]); FHE.allow(bids[msg.sender], msg.sender); // Transfer the highest bid to the beneficiary FHE.allowTransient(highestBid, address(confidentialToken)); confidentialToken.confidentialTransfer(beneficiary, highestBid); // Send the NFT to the winner nftContract.safeTransferFrom(address(this), msg.sender, tokenId); }流程为校验调用者确为赢家且 NFT 未被领取 → 将赢家出价清零FHE.asEuint64(0)平凡加密 0→ 通过allowTransient临时授权代币合约用confidentialTransfer把最高出价密文转给受益人 → 最后把 NFT 转给赢家。整个过程中最高出价金额始终以密文形式流动受益人拿到的是加密余额。败者退款function withdraw(address bidder) public onlyAfterWinnerRevealed { if (bidder winnerAddress) revert TooLateError(auctionEndTime); // Get the user bid value euint64 amount bids[bidder]; FHE.allowTransient(amount, address(confidentialToken)); // Reset user bid value euint64 newBid FHE.asEuint64(0); bids[bidder] newBid; FHE.allowThis(newBid); FHE.allow(newBid, bidder); // Refund the user with their bid amount confidentialToken.confidentialTransfer(bidder, amount); }非赢家包括从未赢过的人可以将自己的出价全额取回。withdraw拒绝赢家调用revert TooLateError且退款后同样把出价记录清零并同步访问权限防止重复取款。用 Hardhat 测试验证整条拍卖链路完整示例sealed-bid-auction.md附带了一套 TypeScript 测试覆盖从铸造机密代币到赢家领奖、败者退款的全流程。运行前请把.sol文件放入项目根目录/contracts/.ts文件放入项目根目录/test/确保 Hardhat 能正确编译与测试。测试中的关键辅助函数展示了 SDK 侧的标准用法// 用 fhevm.createEncryptedInput 在本地加密出价 async function encryptBid(targetContract: string, userAddress: string, amount: number) { const bidInput hre.fhevm.createEncryptedInput(targetContract, userAddress); bidInput.add64(amount); return await bidInput.encrypt(); } // 授权拍卖合约作为代币 operator机密代币的授权机制 async function approve(signer: HardhatEthersSigner) { const approveTx await USDCc.connect(signer).setOperator( blindAuctionAddress, Math.floor(Date.now() / 1000) 60 * 60, ); await approveTx.wait(); } // 提交加密出价handles[0] 是密文句柄inputProof 是零知识证明 async function placeBid(signer: HardhatEthersSigner, amount: number) { const encryptedBid await encryptBid(blindAuctionAddress, signer.address, amount); const bidTx await blindAuction.connect(signer).bid(encryptedBid.handles[0], encryptedBid.inputProof); await bidTx.wait(); }注意bid()的前置条件是调用者必须先对机密代币调用setOperator(auctionAddress, deadline)授权拍卖合约——这与 ERC7984 的机密转账机制一致属于必须在文档/测试中显式处理的链上前置步骤。结算阶段则演示了公开解密的标准调用顺序请求解密 → 解析事件拿到加密句柄 → 调用 Relayer 的publicDecrypt→ 把明文与证明提交回链上async function resolveAuctionViaPublicDecrypt() { const tx await blindAuction.decryptWinningAddress(); const receipt await tx.wait(); // 从 AuctionDecryptionRequested 事件中解析出加密句柄 let encryptedWinningAddress: string | undefined; for (const log of receipt!.logs) { const parsed blindAuction.interface.parseLog(log); if (parsed parsed.name AuctionDecryptionRequested) { encryptedWinningAddress parsed.args.encryptedWinningAddress; break; } } expect(encryptedWinningAddress).to.not.be.undefined; // 调用 Zama Relayer 计算解密结果 const publicDecryptResults await fhevm.publicDecrypt([encryptedWinningAddress!]); // 将解密结果提交回合约进行链上验证 await blindAuction.resolveAuction( publicDecryptResults.abiEncodedClearValues, publicDecryptResults.decryptionProof, ); }核心场景测试 bob should win auction 完整验证了Alice 出价 10,000、Bob 出价 15,000 → 模拟时间推进 1 小时使拍卖结束 → 公开解密揭示 Bob 为赢家 → Bob 领取 NFT 并转移 15,000 给受益人 → Alice 取回 10,000 退款 → Bob 作为赢家无法调用withdraw必须回滚。这一测试同时印证了本教程中所有关键设计密文比较、分支选择、余额差到账、公开解密验证在实际运行中的正确性。小结本教程演示了如何在链上构建一个使用全同态加密的密封式 NFT 拍卖全部出价全程加密出价在本地加密FHE.fromExternal配零知识证明校验后进入合约密文上完成比较FHE.ltFHE.select在不揭示任何金额的前提下维护最高出价与领先者按需揭示拍卖结束后通过makePubliclyDecryptable请求公开解密checkSignatures链上验证 KMS 证明仅公开赢家信息隐私友好的资金流机密 ERC7984 代币承载支付用余额差而非用户输入确定到账金额静默零转账设计避免信息泄露。可以在此基础上继续扩展更复杂的评标逻辑如次高价拍卖、多轮拍卖、与机密投票/机密质押等 FHE 原语组合乃至构建完整的 FHE 去中心化应用。完整合约与测试代码见 sealed-bid-auction.md配套教程即本文所讲解的 sealed-bid-auction-tutorial.md。【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考