news 2026/9/14 15:31:20

WTF Solidity 极简入门:Merkle Tree 与 NFT 白名单发放实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WTF Solidity 极简入门:Merkle Tree 与 NFT 白名单发放实战

WTF Solidity 极简入门:Merkle Tree 与 NFT 白名单发放实战

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

Merkle Tree(默克尔树/哈希树)是区块链的底层加密技术,被比特币与以太坊广泛采用。本篇以 WTF-Solidity 教程第 36 讲为骨架,系统讲解 Merkle Tree 的构造原理、Merkle Proof 验证机制、链上MerkleProof库的实现细节,并给出一个完整的 ERC721 白名单合约与 Remix 部署验证流程——读完你即可用一条根哈希(root)在链上验证海量白名单地址,显著降低 gas 成本。

Merkle Tree 是什么

Merkle Tree,也叫默克尔树或哈希树,是一种自下而上构建的加密树

  • 每个叶子节点是对应数据的哈希(例如某个白名单地址的keccak256哈希);
  • 每个非叶子节点是其两个子节点哈希再次哈希后的结果;
  • 树的顶端是根节点(root / Top Hash),代表整棵树的指纹。

这种结构的核心价值在于:Merkle Tree 允许对大型数据结构的内容进行高效且安全的验证(Merkle Proof)。对于有N个叶子节点的 Merkle Tree,在已知root根值的情况下,验证某个数据是否属于该树(即是否是某个叶子节点),只需要ceil(log₂N)个哈希值(即proof),非常高效。

Merkle Proof 的验证逻辑

看一个直观的例子:假设叶子L1Merkle proofHash 0-1Hash 1。只要知道这两个值,就能验证L1是否在 Merkle Tree 的叶子中:

  1. 通过叶子L1的数据可计算得到Hash 0-0
  2. 已知Hash 0-1,将Hash 0-0Hash 0-1联合哈希得到Hash 0
  3. 已知Hash 1,将Hash 0Hash 1联合哈希得到Top Hash(即 root 节点哈希);
  4. 若最终结果与链上存储的root相等,则证明通过;如果数据有误或proof错误,则无法还原出root根值

生成 Merkle Tree:网页工具与 merkletreejs

生成 Merkle Tree 有两种常用方式:在线网页工具(merkletreejs 的示例页面)或 JavaScript 库merkletreejs

本文使用网页工具生成一棵以4 个地址为叶子节点的 Merkle Tree。叶子节点输入:

[ "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB" ]

在菜单中勾选Keccak-256hashLeavessortPairs三个选项,点击Compute,Merkle Tree 即生成完毕。展开如下:

└─ Root: eeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 ├─ 9d997719c0a5b5f6db9b8ac69a988be57cf324cb9fffd51dc2c37544bb520d65 │ ├─ Leaf0:5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229 │ └─ Leaf1:999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb └─ 4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c ├─ Leaf2:04a10bfd00977f54cc3450c9b25c9b3a502a089eba0097ba35fc33c4ea5fcb54 └─ Leaf3:dfbe3e504ac4e35541bebad4d0e7574668e16fefa26cd4172f93e18b59ce9486

为什么必须勾选sortPairs排序保证哈希运算满足交换律(即H(a,b) == H(b,a))。在链上验证时,_hashPair会先对两个节点哈希排序再哈希,链下生成时也必须用同样的排序规则,否则链下链上的树不一致,验证必然失败。这一点在仓库源码的注释中也明确强调(见 MerkleTree.sol 头部注释)。

通过网页工具还可以拿到地址 0 的proof(即上图中其所有兄弟节点的哈希):

[ "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c" ]

链上验证:MerkleProof 库实现剖析

WTF-Solidity 在第 36 讲给出了一个完整的MerkleProof库(与 OpenZeppelin 的经典实现一致),完整源码见 36_MerkleTree/MerkleTree.sol:

library MerkleProof { function verify( bytes32[] memory proof, bytes32 root, bytes32 leaf ) internal pure returns (bool) { return processProof(proof, leaf) == root; } function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { computedHash = _hashPair(computedHash, proof[i]); } return computedHash; } // Sorted Pair Hash function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); } }

库中包含三个函数,职责逐层递进:

  1. verify()函数:利用proof验证leaf是否属于根为root的 Merkle Tree,属于则返回true。它内部调用processProof()
  2. processProof()函数:利用proofleaf从叶子向根逐层计算,还原出 Merkle Tree 的root。它内部调用_hashPair()。从源码看,其核心是一个循环:computedHashleaf出发,每轮与一个 proof 节点做一次配对哈希,循环次数等于proof.length
  3. _hashPair()函数:用keccak256()计算两个子节点的哈希,且先排序再哈希a < bkeccak256(abi.encodePacked(a, b)),否则反过来),这是保证交换律、消除节点顺序歧义的关键。

验证流程与安全特性

地址0的叶子哈希、root与对应proof输入verify()函数,将返回true;如果改变了其中任意一个值(叶子数据、proof 中的任一兄弟哈希、或 root),都无法还原出正确的根,返回false

从源码结构还可以推断两个安全要点:

  • 防重入设计:在mint()中,地址被记录到mintedAddress的赋值发生在_mint之前,这种"先置位、后转账"的顺序可有效阻止重入攻击(WTF-Solidity 在 S01_ReentrancyAttack/readme.md 中有专题讲解)。
  • 仅存 root、轻量上链leafproof由链下(后端)持有,链上合约只存储一个root,因此验证开销与白名单规模无关,极其省 gas。

与 OpenZeppelin 实现的对照

本仓库lib目录下还内置了 OpenZeppelin 的完整版 MerkleProof.sol。对照可以看出:

  • 本讲的精简实现与 OZ 的verify/processProof逻辑完全一致;
  • OZ 版本额外提供了verifyCalldata(proof 使用calldata传入,进一步节省 gas)、支持自定义哈希函数的verify(..., hasher)重载,以及用于同时验证多个叶子的multiProofVerify/processMultiProof(需要proofFlags标记合并分支);
  • OZ 版本还给出了重要的安全警告:应避免使用 64 字节长的叶子值,否则排序后的内部节点对拼接结果可能被误解读为叶子值,构成潜在攻击面。OZ 的 JavaScript 库生成 Merkle 树时默认规避了该风险。

对绝大多数白名单场景,本讲的精简版已足够;若需要批量空投证明,可升级使用 OZ 的 multiproof 能力。

实战:用 Merkle Tree 发放 NFT 白名单

为什么用 Merkle Tree 发白名单

一份拥有800 个地址的白名单,如果直接把地址数组存到链上,更新一次所需的 gas 费用很容易超过1 ETH。而采用 Merkle Tree 后:

  • leaf(地址哈希)和proof可以存放在后端或客户端;
  • 链上只需存储一个root(32 字节);
  • 每次白名单更新只改变 root,gas 开销极小。

因此很多ERC721标准的 NFT 与ERC20标准代币的白名单/空投都采用 Merkle Tree 发放(例如 Optimism 的空投)。

MerkleTree 白名单合约

仓库中的完整合约见 36_MerkleTree/MerkleTree.sol,该合约继承ERC721标准(实现见 34_ERC721/ERC721.sol)并利用MerkleProof库:

contract MerkleTree is ERC721 { bytes32 immutable public root; // Merkle树的根 mapping(address => bool) public mintedAddress; // 记录已经mint的地址 // 构造函数,初始化NFT合集的名称、代号、Merkle树的根 constructor(string memory name, string memory symbol, bytes32 merkleroot) ERC721(name, symbol) { root = merkleroot; } // 利用Merkle树验证地址并完成mint function mint(address account, uint256 tokenId, bytes32[] calldata proof) external { require(_verify(_leaf(account), proof), "Invalid merkle proof"); // Merkle检验通过 require(!mintedAddress[account], "Already minted!"); // 地址没有mint过 mintedAddress[account] = true; // 记录mint过的地址 _mint(account, tokenId); // mint } // 计算Merkle树叶子的哈希值 function _leaf(address account) internal pure returns (bytes32) { return keccak256(abi.encodePacked(account)); } // Merkle树验证,调用MerkleProof库的verify()函数 function _verify(bytes32 leaf, bytes32[] memory proof) internal view returns (bool) { return MerkleProof.verify(proof, root, leaf); } }

状态变量

合约中有两个状态变量:

  • rootbytes32 immutable类型,存储 Merkle Tree 的根,部署合约时在构造函数中赋值(immutable意味着部署后不可更改,保证白名单根哈希不可被篡改);
  • mintedAddressmapping(address => bool),记录已 mint 过的地址,某地址 mint 成功后置为true,用于防止同一地址重复铸造。

四个核心函数

合约中共有 4 个函数:

  • 构造函数:初始化 NFT 的名称(name)、代号(symbol)和 Merkle Tree 的rootERC721(name, symbol)基类构造函数负责存储名称与代号,本合约只需把merkleroot写入root
  • mint()函数:利用白名单铸造 NFT。参数为白名单地址account、铸造的tokenIdproof。执行顺序是:先调用_verify(_leaf(account), proof)验证地址是否在白名单中,再检查mintedAddress确认该地址未铸造过,然后先把地址记录到mintedAddress(防止重入攻击),最后调用基类_mint(account, tokenId)铸造 NFT。此过程调用了_leaf()_verify()函数。
  • _leaf()函数:计算 Merkle Tree 叶子地址的哈希,即keccak256(abi.encodePacked(account))——注意此处只对地址本身取哈希,不包含任何字符串前缀或类型标记,因此链下生成树时hashLeaves选项必须与之一致。
  • _verify()函数:调用MerkleProof库的verify()函数进行 Merkle Tree 验证,参数为叶子哈希、proof 与合约内存储的root

Remix 验证全流程

第一步:部署合约

使用上文例子中的 4 个地址作为白名单并生成 Merkle Tree。部署MerkleTree合约,3 个参数分别为:

name = "WTF MerkleTree" symbol = "WTF" merkleroot = 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097

merkleroot正是网页工具生成结果中的根值,与仓库源码 MerkleTree.sol 头部注释中的示例数据一一对应。

第二步:白名单 mint

接下来运行mint函数给地址 0 铸造 NFT,3 个参数分别为:

account = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 tokenId = 0 proof = [ "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c" ]

交易成功提交后,可调用 ERC721 基类提供的ownerOf函数验证:tokenId为 0 的 NFT 已经铸造给了地址 0(ownerOf的实现在 34_ERC721/ERC721.sol 中,它从_owners映射读取持有者,若为 0 地址则 revert"token doesn't exist"),合约运行成功。

第三步:验证防重放

此时若再次调用mint函数给同一地址铸造,虽然该地址能够通过 Merkle Proof 验证(它确实在白名单中、proof 也正确),但由于地址已经记录在mintedAddress中,require(!mintedAddress[account], "Already minted!")会触发,交易将被中止并回滚。

总结

这一讲围绕 WTF-Solidity 第 36 讲展开,完成了三件事:

  1. 理解概念:Merkle Tree 是自下而上构建的哈希树,支持用log(N)级别的 proof 高效验证数据归属;
  2. 掌握实现:分析了 MerkleProof 库 中verify/processProof/_hashPair三层函数的调用链,并对照 OpenZeppelin 完整版 说明了calldata变体与 multiproof 的扩展能力;
  3. 完成实战:基于 ERC721 基类 实现并部署了 MerkleTree 白名单合约,在 Remix 中完成了从生成树、部署到白名单 mint、防重放验证的完整闭环。

在实际生产中,复杂的 Merkle Tree 可以利用 JavaScript 库merkletreejs生成和管理,链上只需要存储一个 root 值,非常节省 gas,因此大量项目方选择使用 Merkle Tree 来发放白名单与空投。若想进一步了解白名单之外的安全主题,可继续阅读仓库中的 S01_ReentrancyAttack 等安全专题。

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 15:31:11

基于Matlab的微电网日前-日内两阶段优化调度与敏感性分析实现

做微电网优化调度的时候&#xff0c;最让人头疼的往往不是模型本身有多复杂&#xff0c;而是“算完一个结果”和“结果真的敢用”之间隔着一道鸿沟。今天想聊的这个项目&#xff0c;就是一个典型的“日前-日内两阶段优化调度”框架&#xff0c;外加对电价、光伏、风电、负荷四个…

作者头像 李华
网站建设 2026/9/14 15:25:19

手机发烫卡顿?锁帧是降低功耗、稳住游戏帧率的实用技巧

前10分钟手感火热&#xff0c;10分钟之后手指头先遭罪&#xff0c;机身烫得像刚出炉的烤地瓜。这几年我帮身边朋友调手机、看各种反馈&#xff0c;大家面对发烫的第一反应都特别统一&#xff1a;买散热背夹、摘手机壳、关后台&#xff0c;忙活一圈之后发现&#xff0c;真正横在…

作者头像 李华
网站建设 2026/9/14 15:25:06

基于Spring Boot的快递物流信息查询系统设计与实现

我前阵子帮一位准备答辩的学弟梳理项目&#xff0c;他说想做“基于web的快递物流信息查询系统”。当时我就觉得这个选题很有意思——它不像纯粹的CRUD增删改查那样单薄&#xff0c;也不像电商系统那样庞大难以收尾&#xff0c;正好卡在一个恰到好处的位置&#xff1a;前台用户查…

作者头像 李华
网站建设 2026/9/14 15:24:13

DDR2 SDRAM控制器VHDL实现与Spartan-2时序收敛实战

简介&#xff1a;本资源是一套面向FPGA初学者与数字电路设计者的DDR2 SDRAM控制器完整实现方案&#xff0c;聚焦Xilinx Spartan-2系列器件&#xff0c;解决高速存储器接口在FPGA上的时序建模、命令调度与数据通路设计等核心问题。压缩包共126个文件&#xff0c;含46个Verilog&a…

作者头像 李华
网站建设 2026/9/14 15:24:08

deer-flow:基于页表管控的轻量级内存沙盒原理与实践

1. “deer-flow”不是框架&#xff0c;是内存沙盒的命名隐喻第一次在 GitHub 上看到deer-flow这个仓库名时&#xff0c;我下意识搜了三遍——没有文档、没有 README、没有 star 数&#xff0c;连 issue 都是空的。但它的 commit 记录里反复出现mem.c(776)、out of memory、0xc0…

作者头像 李华