Solidity与Firebase函数Merkle树叶子计算不一致问题求助
Merkle树叶子节点计算不一致导致合约验证失败的排查与修复
问题背景
我正在开发一个集成Merkle树白名单功能的NFT智能合约,流程为:Firebase函数接收用户地址与预留代币数量,生成Merkle Proof返回前端,前端再调用合约的mint函数。当前核心问题是Firebase侧与Solidity合约侧的叶子节点计算逻辑不匹配,导致合约始终验证Proof失败:
- 合约侧:通过
keccak256(abi.encodePacked(msg.sender, _addressReservedTokens))将地址(address类型)和预留数量(uint256类型)打包后哈希生成叶子 - Firebase侧:实际代码仅对地址单独做哈希,完全未结合预留数量,且地址格式存在错误
双方代码对比
Solidity合约代码
// 1. 地址校验:验证proof是否有效 function isPrivateListed( uint256 _addressReservedTokens, bytes32[] memory _proof ) public view returns (bool) { bytes32 leaf = keccak256(abi.encodePacked(msg.sender, _addressReservedTokens)); return MerkleProof.verify(_proof, privateListRoot, leaf); } // 2. 前端调用的Mint函数 function privateMint( uint256 _mintAmount, uint256 _addressReservedTokens, bytes32[] calldata proof ) public payable { require(privateOpen, "The private mint is not opened"); require( isPrivateListed(_addressReservedTokens, proof), "you are not in the private list" ); uint256 supply = totalSupply(); require(supply + _mintAmount <= maxSupply, "max NFT limit exceeded"); uint256 owned = addressMintedBalance[msg.sender]; require( owned + _mintAmount <= _addressReservedTokens, "You have less nft reserved" ); for (uint256 i = 1; i <= _mintAmount; i++) { _safeMint(msg.sender, supply + i); addressMintedBalance[msg.sender]++; } }
Firebase函数代码(原始错误版本)
// 1. 从客户端调用的函数中获取参数 const walletAddress = data.walletAddress; let whitelistAddresses = [ '0x38207d0e84edb04a57482c3769fe1926175203733', '0x943926a8ff0000350d0b879a658fa52bcd4fca186', ]; // 2. 通过keccak256哈希`whitelistAddresses`的所有元素,创建`leafNodes`数组;再以keccak256为算法创建Merkle树对象 const leafNodes = whitelistAddresses.map((addr) => keccak256(addr)); const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true, }); // 3. 获取Merkle树的根哈希,转换为十六进制格式(0x开头) const rootHash = merkleTree.getRoot(); const rootHashHex = merkleTree.getHexRoot(); // ***** ***** ***** 客户端侧 ***** ***** ***** // const claimingAddress = keccak256(walletAddress); const hexProof = merkleTree.getHexProof(claimingAddress); const verifyProof = merkleTree.verify(hexProof, claimingAddress, rootHash);
核心差异分析
- 数据组合缺失:Firebase代码仅对地址单独哈希,完全未结合预留代币数量,但合约侧是将地址+预留数量一起打包哈希,这是最关键的不匹配点
- 数据编码规则不一致:Solidity的
abi.encodePacked会按原始字节长度拼接address(20字节)和uint256(32字节),而直接拼接字符串的字节流与该规则完全不同 - 地址格式错误:Firebase中的测试地址长度不符合标准(正常以太坊地址为20字节,示例中多了1位),会直接导致哈希结果错误
修复方案(修正Firebase代码)
需要完全对齐Solidity侧的叶子节点计算逻辑,使用ethers.js模拟abi.encodePacked的行为:
const { ethers } = require("ethers"); // 1. 从客户端获取参数:需同时获取地址和对应预留数量 const walletAddress = data.walletAddress; const userReservedAmount = data.reservedAmount; // 2. 修正白名单数据:使用标准地址,每个地址绑定对应预留数量 const whitelistEntries = [ { address: '0x38207d0e84edb04a57482c3769fe192617520373', reserved: 5 }, { address: '0x943926a8ff0000350d0b879a658fa52bcd4fca18', reserved: 3 }, ]; // 3. 按照Solidity abi.encodePacked规则生成叶子节点 const leafNodes = whitelistEntries.map(entry => { // 模拟合约侧的打包逻辑 const packedData = ethers.utils.solidityPack( ["address", "uint256"], [entry.address, entry.reserved] ); return ethers.utils.keccak256(packedData); }); // 4. 创建Merkle树(保持sortPairs参数与合约侧一致) const merkleTree = new MerkleTree(leafNodes, ethers.utils.keccak256, { sortPairs: true, }); // 5. 生成当前用户的叶子节点并获取Proof const userLeaf = ethers.utils.keccak256( ethers.utils.solidityPack(["address", "uint256"], [walletAddress, userReservedAmount]) ); const hexProof = merkleTree.getHexProof(userLeaf); // 本地验证Proof(可选) const rootHashHex = merkleTree.getHexRoot(); const verifyResult = merkleTree.verify(hexProof, userLeaf, rootHashHex);
额外注意事项
- 确保合约部署时,
privateListRoot设置为Firebase生成的rootHashHex - 前端调用
privateMint时,传入的_addressReservedTokens必须与Firebase生成叶子时使用的预留数量完全一致 - 保持Merkle树的
sortPairs参数在Firebase与合约侧(若MerkleProof库有配置)完全相同
内容的提问来源于stack exchange,提问作者Pietro Ciattaglia
相关产品推荐
相关产品推荐

