36. 默克尔树
WTF Solidity极简入门: 36. 默克尔树 Merkle Tree
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
这一讲,我将介绍Merkle Tree
,以及如何利用Merkle Tree
发放NFT
白名单。
Merkle Tree
Merkle Tree
,也叫默克尔树或哈希树,是区块链的底层加密技术,被比特币和以太坊区块链广泛采用。Merkle Tree
是一种自下而上构建的加密树,每个叶子是对应数据的哈希,而每个非叶子为它的2
个子节点的哈希。
Merkle Tree
允许对大型数据结构的内容进行有效和安全的验证(Merkle Proof
)。对于有N
个叶子节点的Merkle Tree
,在已知root
根值的情况下,验证某个数据是否有效(属于Merkle Tree
叶子节点)只需要ceil(log₂N)
个数据(也叫proof
),非常高效。如果数据有误,或者给的proof
错误,则无法还原出root
根值。
下面的例子中,叶子L1
的Merkle proof
为Hash 0-1
和Hash 1
:知道这两个值,就能验证L1
的值是不是在Merkle Tree
的叶子中。为什么呢?
因为通过叶子L1
我们就可以算出Hash 0-0
,我们又知道了Hash 0-1
,那么Hash 0-0
和Hash 0-1
就可以联合算出Hash 0
,然后我们又知道Hash 1
,Hash 0
和Hash 1
就可以联合算出Top Hash
,也就是root节点的hash。
生成Merkle Tree
我们可以利用网页或者Javascript库merkletreejs来生成Merkle Tree
。
这里我们用网页来生成4
个地址作为叶子节点的Merkle Tree
。叶子节点输入:
1 | [ |
在菜单里选上Keccak-256
, hashLeaves
和sortPairs
选项,然后点击Compute
,Merkle Tree
就生成好了。Merkle Tree
展开为:
1 | └─ 根: eeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 |
Merkle Proof
验证
通过网站,我们可以得到地址0
的proof
如下,即图2中蓝色节点的哈希值:
1 | [ |
我们利用MerkleProof
库来验证:
1 | library MerkleProof { |
MerkleProof
库有三个函数:
-
verify()
函数:利用proof
数来验证leaf
是否属于根为root
的Merkle Tree
中,如果是,则返回true
。它调用了processProof()
函数。 -
processProof()
函数:利用proof
和leaf
依次计算出Merkle Tree
的root
。它调用了_hashPair()
函数。 -
_hashPair()
函数:用keccak256()
函数计算非根节点对应的两个子节点的哈希(排序后)。
我们将地址0
的Hash
,root
和对应的proof
输入到verify()
函数,将返回true
。因为地址0
的Hash
在根为root
的Merkle Tree
中,且proof
正确。如果改变了其中任意一个值,都将返回false
。
利用Merkle Tree
发放NFT
白名单
一份拥有800个地址的白名单,更新一次所需的gas fee很容易超过1个ETH。而由于Merkle Tree
验证时,leaf
和proof
可以存在后端,链上仅需存储一个root
的值,非常节省gas
,项目方经常用它来发放白名单。很多ERC721
标准的NFT
和ERC20
标准代币的白名单/空投都是利用Merkle Tree
发出的,比如optimism
的空投。
这里,我们介绍如何利用MerkleTree
合约来发放NFT
白名单:
1 | contract MerkleTree is ERC721 { |
MerkleTree
合约继承了ERC721
标准,并利用了MerkleProof
库。
状态变量
合约中共有两个状态变量:
root
存储了Merkle Tree
的根,部署合约的时候赋值。mintedAddress
是一个mapping
,记录了已经mint
过的地址,某地址mint成功后进行赋值。
函数
合约中共有4个函数:
- 构造函数:初始化
NFT
的名称和代号,还有Merkle Tree
的root
。 mint()
函数:利用白名单铸造NFT
。参数为白名单地址account
,铸造的tokenId
,和proof
。首先验证address
是否在白名单中,然后验证该地址是否还未铸造,验证通过则先把该地址记录到mintedAddress
中防止重入攻击,然后把序号为tokenId
的NFT
铸造给该地址。此过程中调用了_leaf()
和_verify()
函数。_leaf()
函数:计算了Merkle Tree
的叶子地址的哈希。_verify()
函数:调用了MerkleProof
库的verify()
函数,进行Merkle Tree
验证。
remix
验证
我们使用上面例子的4
个地址作为白名单并生成Merkle Tree
。我们部署MerkleTree
合约,3
个参数分别为:
1 | name = "WTF MerkleTree" |
接下来运行mint
函数给地址0
铸造NFT
,3
个参数分别为:
1 | account = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 |
我们可以用ownerOf
函数验证tokenId
为0的NFT
已经铸造给了地址0
,合约运行成功!
此时,若再次调用mint函数,虽然该地址能够通过Merkle Proof
验证,但由于地址已经记录在mintedAddress
中,因此该交易会由于"Already minted!"
被中止。
总结
这一讲,我们介绍了Merkle Tree
的概念,如何生成简单的Merkle Tree
,如何利用智能合约验证Merkle Tree
,以及用它来发放NFT
白名单。
在实际使用中,复杂的Merkle Tree
可以利用javascript
库merkletreejs
来生成和管理,链上只需要存储一个根值,非常节省gas
。很多项目方都选择利用Merkle Tree
来发放白名单。