# Merkle Tree **Published by:** [startha](https://paragraph.com/@starha/) **Published on:** 2023-11-14 **URL:** https://paragraph.com/@starha/merkle-tree ## Content Merkle Tree,也叫默克尔树或哈希树,是区块链的底层加密技术,被比特币和以太坊区块链广泛使用。Merkle Tree是一种自下而上的加密树,每个叶子是对应数据的哈希,而每个叶子为它的2个子节点的哈希。Merkle Tree允许对大型数据结构的内容进行有效和安全的验证(Merkle Proof)。对于有N个节点的Merkle Tree,在已知root根值得情况下,验证某个数据是否有效(属于Merkle Tree叶子节点)只需要log(N)个数据(也叫proof),非常高效。如果数据有误,或者给的proof错误,则无法还原出root根值。下面的例子中,叶子L1的Merkle proof为Hash 0-1和Hash 1:知道这两个值,就能验证L1的值是不是在MMerkle Tree的叶子中。为什么呢?因为通过叶子L1我们就可以算出Hash 0-0,我们又知道Hash 0-1,那么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。叶子节点输入: [ "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB" ] 在菜单里选上Keccak-256, hashLeaves和sortPairs选项,然后点击Compute,Merkle Tree就生成好了。Merkle Tree展开为:└─ 根: eeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 ├─ 9d997719c0a5b5f6db9b8ac69a988be57cf324cb9fffd51dc2c37544bb520d65 │ ├─ 叶子0:5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229 │ └─ 叶子1:999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb └─ 4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c ├─ 叶子2:04a10bfd00977f54cc3450c9b25c9b3a502a089eba0097ba35fc33c4ea5fcb54 └─ 叶子3:dfbe3e504ac4e35541bebad4d0e7574668e16fefa26cd4172f93e18b59ce9486 Merkle Proof验证通过网址,我们可以看到地址0的proof如下。[ "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"] 我们利用MerkleProof库来验证:library MerkleProof { /** * @dev 当通过`proof`和`leaf`重建出的`root`与给定的`root`相等时,返回`true`,数据有效。 * 在重建时,叶子节点对和元素对都是排序过的。 */ function verify( bytes32[] memory proof, bytes32 root, bytes32 leaf ) internal pure returns (bool) { return processProof(proof, leaf) == root; } /** * @dev Returns 通过Merkle树用`leaf`和`proof`计算出`root`. 当重建出的`root`和给定的`root`相同时,`proof`才是有效的。 * 在重建时,叶子节点对和元素对都是排序过的。 */ 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)); } } MerkleProof库有三个函数:verify()函数:利用proof数来验证leaf是否属于根为root的Merkle Tree中,如果是,则返回true。它调用了processProof()函数。processProof()函数:利用proof和leaf依次计算出Merkle Tree的root。它调用了_hashPair()函数。_hashPair()函数:用keccak256()函数计算非根节点对应的两个子节点的哈希(排序后)。我们将地址0,root和对应的proof输入到verify()函数,将返回true。因为地址0在根为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白名单: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过 _mint(account, tokenId); // mint mintedAddress[account] = true; // 记录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); } } MerkleTree合约继承了ERC721标准,并利用了MerkleProof库。状态变量合约中共有两个状态变量:root存储了Merkle Tree的根,部署合约的时候赋值。mintedAddress是一个mapping,记录了已经mint过的地址,某地址mint成功后进行赋值。函数合约中共有4个函数:构造函数:初始化NFT的名称和代号,还有Merkle Tree的root。mint()函数:利用白名单铸造NFT。参数为白名单地址account,铸造的tokenId,和proof。首先验证address是否在白名单中,验证通过则把序号为tokenId的NFT铸造给该地址,并将它记录到mintedAddress。此过程中调用了_leaf()和_verify()函数。_leaf()函数:计算了Merkle Tree的叶子地址的哈希。_verify()函数:调用了MerkleProof库的verify()函数,进行Merkle Tree验证。remix验证我们使用上面例子的4个地址作为白名单并生成Merkle Tree。我们部署MerkleTree合约,3个参数分别为:name = "STAR MerkleTree" symbol = "STAR" merkleroot = 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 接下来运行mint函数给地址0铸造NFT,3个参数分别为:account = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 tokenId = 0 proof = [ "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c" ] 我们可以用ownerOf函数验证tokenId为0的NFT已经铸造给了地址0,合约运行成功!此时,若再次调用mint函数,虽然该地址能够通过Merkle Proof验证,但由于地址已经记录在mintedAddress中,因此该交易会由于"Already minted!"被中止。 ## Publication Information - [startha](https://paragraph.com/@starha/): Publication homepage - [All Posts](https://paragraph.com/@starha/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@starha): Subscribe to updates