如何用 OpenZeppelin Contracts MerkleProof 在链上验证白名单 Merkle 证明?
【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts
白名单空投、限量铸造等场景里,完整的名单放在链上成本太高,常见做法是:把名单的 Merkle 树在链下构建好,只把根哈希(root)写入合约,用户上链时提交自己所在叶子的证明(proof),合约用 OpenZeppelin Contracts 的 MerkleProof 库验证"这个叶子确实属于这棵树"。本文基于当前仓库的实现(Solidity 要求^0.8.20)走一遍完整路径:链下建树取证明 → 链上写验证合约 → 按项目测试用例的判断标准核对验证结果。
链下:构建白名单树并生成证明
MerkleProof.sol 的文件头注释和 utilities.adoc 都指出,树和证明可以用 OpenZeppelin 的merkle-treeJavaScript 库在链下生成,该库"开箱即用地"规避了 64 字节叶子攻击(见下文"边界与限制")。项目自己的测试 MerkleProof.test.js 演示了最直接的用法:
const { SimpleMerkleTree } = require('@openzeppelin/merkle-tree'); const { ethers } = require('hardhat'); // 测试文件 toLeaves:把每个白名单条目转成 keccak256 哈希作为叶子 const toLeaves = (str, separator = '') => str.split(separator).map(e => ethers.keccak256(ethers.toUtf8Bytes(e))); // 测试文件 defaultHash:内部节点取"排序后拼接"的 keccak256 const concatSorted = (...elements) => Buffer.concat(elements.map(ethers.getBytes).sort(Buffer.compare)); const nodeHash = (a, b) => ethers.keccak256(concatSorted(a, b)); const merkleTree = SimpleMerkleTree.of(toLeaves('abc'), { nodeHash }); // 'abc' 仅为文档示例,实际替换为你的白名单条目 const root = merkleTree.root; // 部署时写入合约 const hash = merkleTree.at(0); // 第 0 个条目的叶子哈希(即链上验证的 leaf 参数) const proof = merkleTree.getProof(0); // 该叶子到根的兄弟哈希数组产出三样东西:root(上链)、hash(叶子的 keccak256 哈希)、proof(兄弟哈希数组)。注意叶子不是条目原文,而是ethers.keccak256(ethers.toUtf8Bytes(条目))——链上合约收到的leaf参数必须与这个构造方式一致,否则验证必然失败。
链上:写入 root 并调用 verify 验证
库提供两组等价函数(均支持传入自定义哈希函数hasher的重载):
verify/processProof:proof 在内存中处理,默认哈希函数;verifyCalldata/processProofCalldata:proof 直接在 calldata 中处理,调用方传入证明时优先用这一组(MerkleProof.sol 的注释区分了二者)。
默认路径下(未传hasher),库内部用Hashes.commutativeKeccak256逐层把当前哈希与兄弟哈希"排序后拼接再 keccak256",与上面链下defaultHash的规则一致。仓库的 remappings.txt 已把@openzeppelin/contracts/映射到contracts/,可直接按包名导入。一个最小可部署的白名单验证合约(示例中root_用链下merkleTree.root的值替换):
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; contract WhitelistMerkle { bytes32 public immutable merkleRoot; constructor(bytes32 root_) { merkleRoot = root_; } /// proof 是链下 getProof 生成的兄弟哈希数组,leaf 是白名单条目的 keccak256 哈希 function verifyWhitelist(bytes32[] calldata proof, bytes32 leaf) external view returns (bool) { return MerkleProof.verifyCalldata(proof, merkleRoot, leaf); } }如果你的业务需要"一次提交多个白名单条目"(例如批量领取),可选分支是使用 multiproof 系列:multiProofVerifyCalldata(proof, proofFlags, root, leaves),参数对应链下getMultiProof的输出:
const { proof, proofFlags, leaves } = merkleTree.getMultiProof(toLeaves('bdf')); const hashes = leaves.map(e => merkleTree.leafHash(e)); // 每个叶子的哈希,作为链上 leaves 参数multiproof 有明确适用条件(MerkleProof.sol 注释):树必须是完整的(complete,但不必完美),且待证明叶子按"从最深层由右向左、逐层继续"的逆序排列。
如何判断验证成功或失败
verify的判定逻辑是:processProof从leaf出发、按proof逐层重建根哈希,重建结果等于root即返回true。MerkleProof.test.js 覆盖了三种判断基准,可直接作为自查标准:
- 合法证明:
processProof(proof, hash)的返回值等于root,verify(proof, root, hash)返回true(L26-L36); - 别的树的证明:用另一棵树(
makeTree('def'))的 proof 去对第一棵树的 root 验证,verify返回false(L47-L58); - 长度不对的证明:把 proof 截短一段(
proof.slice(0, -1))后验证,verify返回false(L61-L73)。
注意单条verify对无效输入是返回false而不是 revert;只有 multiproof 在参数个数不满足leaves.length + proof.length == proofFlags.length + 1时才会以自定义错误MerkleProofInvalidMultiproofrevert(MerkleProof.sol L216-L218)。
边界与限制
- 64 字节叶子警告:库头注释明确要求,用 keccak256 哈希叶子时避免使用"哈希前恰好 64 字节"的叶子值,否则两个内部节点排序拼接后可能被重新解释成另一个叶子,从而为树外值构造出"合法"证明;OpenZeppelin 的 JavaScript 库生成的树天然规避此问题(MerkleProof.sol L16-L21)。
- 仅支持交换律哈希函数:proof 验证只支持
H(a, b) == H(b, a)的哈希函数;非交换律树需要库未提供的额外逻辑(L26-L29)。要使用自定义交换律哈希(例如链下用sha256(abi.encode(a, b))的变体),把hasher作为最后一个参数传入verify/verifyCalldata/multiProofVerify*,仓库里的 MerkleProofCustomHashMock 是现成的接法示例。 - multiproof 的空叶子陷阱:
proof.length == 1 && leaves.length == 0的空集合情形被视为合法 no-op 并返回proof[0](即 root),若不在别处校验叶子,攻击者可借此"证明"空名单;库注释建议禁止该输入(L199-L201)。 - 链上建树是另一条路线:如果树本身需要维护在链上(可追加、可更新叶子),应使用 MerkleTree 库而非 MerkleProof,本文场景(白名单一次性写 root)不需要它。
验证合约部署后,用链下生成的(proof, leaf)调用verifyWhitelist,返回true即说明该条目在 root 对应的白名单树内;对树外条目或篡改过的 proof,按测试基准应得到false。如果你后续要把验证嵌入 mint 逻辑,记得把verifyWhitelist的布尔结果接上 revert 或状态标记,合约本身不会替你阻断调用。
【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考