# NFT白名单校验-Merkle Tree

By [cyptoJune](https://paragraph.com/@cyptojune) · 2022-08-31

---

**简介**

白名单是推广NFT项目和奖励早期进入及热情参与者的好方法，有很多方法可以实现白名单发放的机制，每种方法都有自己的优势和劣势。今天就来介绍用Merkle Tree发放白名单。一份拥有800个地址的白名单，更新一次所需的gas fee在市场活跃期，很容易超过1Eth,**那么用Merkel Tree的结构结合Hash函数的特性，就能很容易的在节省gas的前提下，进行白名单的发放**。

**流程逻辑**

1.  根据所有白名单地址计算出 Merkle Root，并设置到合约中
    
2.  前端会根据用户地址请求后端获取对应 Merkle Proof
    
3.  将 Proof 传入合约进行校验
    

**具体逻辑细节**

我们知道，**链上空间的存储Storage和计算是很费gas的，不必要的链上存储和计算尽量避免，尽可能通过链下的方式来实现，并通过前后端的配合实现必要信息的校验和计算。**

1.  **根据所有白名单地址计算出 Merkle Root，并设置到合约中**
    
    在实际的开发中，可以通过javascript库merkletree.js根据白名单来链下生成Merkle Tree，包括Merkle Root，然后合约管理员将生成的 Merkle Root 设置到合约中。
    
    一般通过构造器函数或者设置外部函数进行部署时放置：
    
        constructor(string memory name, string memory symbol, bytes32 merkleroot)
            ERC721(name, symbol)
            {
                root = merkleroot;
            }
        function setRoot(bytes32 _root) external onlyOwner {
            root = _root;
        }
        
    
2.  **前端会根据用户地址请求后端获取对应 Merkle Proof**
    
    前端（或者后端）根据当前的用户，生成一个 Merkle Proof。将 Proof 作为参数传入合约中，与 **msg.sender（实际开发中用msg.sender作为参数进行传入是正确的范式，因为这样保证了安全，不然不是白名单的地址就可以传递白名单地址来给自己 mint）** 和之前设置的 **Merkle Root** 进行校验。
    
    根据步骤一，我们可以通过npm包管理工具install merkletree.js，代码大概如下：
    
        const { MerkleTree } = require('merkletreejs');
        const keccak256 = require('keccak256');
        
        // 白名单地址，这里采用了硬编码，实际开发应该从数据库读取
        // 这里我们随机生成几个地址，作为白名单示例
        let whitelistAddresses = [
            "0x978DCD67B155b3dBecd221Ec0D193f6fa7d3B8c2",
            "0x41fed4790A6137083fac595e00090b2D01d012b6",
            "0xFbC43c738d17F4d43627B8675A8cdC691A603BB3",
            "0xBD925b9Fab6Eb9f713238Cc688A91a7f5c7Ff4c8",
            "0xc6c74C251aa41FCB0De4c55fb751eec04f66774A"
        ]
        
        // 计算 leaf 叶子结点的数据
        const leafNodes = whitelistAddresses.map(addr => keccak256(addr));
        // 生成 Merkle Tree
        const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });
        
        // 获取 Merkle Root
        const rootHash = merkleTree.getRoot();
        // 打印查看 Merkle Tree 全部数据
        console.log('Whitelist Merkle Tree\n', merkleTree.toString());
        // 打印查看 Root 数据，需要设置到合约中
        console.log("Root Hash: ", rootHash.toString('hex'));
        
        // 选择一个白名单地址进行校验
        const claimingAddress = keccak256("0xc6c74C251aa41FCB0De4c55fb751eec04f66774A");
        
        // 计算这个地址的 Merkle Proof，注意这就是我们要传给合约的参数 Proof
        const hexProof = merkleTree.getHexProof(claimingAddress);
        console.log(hexProof);
        
        // 校验
        console.log(merkleTree.verify(hexProof, claimingAddress, rootHash));
        
    
    其中，claimingAddress 地址通过hash之后，通过merkleTree.getHexProof就可以拿到对应地址的Merkle Proof
    
        // 选择一个白名单地址进行校验
        const claimingAddress = keccak256("0xc6c74C251aa41FCB0De4c55fb751eec04f66774A");
        
        // 计算这个地址的 Merkle Proof，注意这就是我们要传给合约的参数 Proof
        const hexProof = merkleTree.getHexProof(claimingAddress);
        console.log(hexProof);
        
    
3.  **将 Proof 传入合约进行校验**
    
    合约校验的逻辑如下：
    

![Merkle Tree结构](https://storage.googleapis.com/papyrus_images/ceeef4b691401ddf2c5e7f8c9ca0619b5110943196baff117d069547045cee72.webp)

Merkle Tree结构

假设如果我们想校验当前地址和L1是否相等，那么通过提取Hash 0-1，Hash 1，以及被测试的地址。然后，按照hash规则，将hash的输出与Top Hash（默克尔Merkle root）进行比较。如果结果相同，那么可以确保L1等于输入地址，也即白名单地址。**那么其他节点，其实是不需要知道的，从而可以将其他不相关的节点都可以从链上拿下来，减少链上存储空间的使用，从而达到节省gas的目的**。

    // 校验
    console.log(merkleTree.verify(hexProof, claimingAddress, rootHash));
    

通过merkleTree.verify函数从而校验是否是白名单！

**总结**

通过Merkle Tree方法校验白名单： 优点是：经济效益高（对项目方来说），易于验证 缺点是：对用户来说，铸造 gas 成本的略大，以及每次希望改变白名单时，都需要重新设 置Merkle Tree 根Root.

**例子**

一个简单的利用MerkleTree合约来发放NFT白名单： `MerkleTree`合约继承了`ERC721`标准，并利用了`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过
            _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);
        }
    }
    

**解释：**

*   状态变量
    
    1.  `root`存储了`Merkle Tree`的根，部署合约的时候赋值
        
    2.  `mintedAddress`是一个`mapping`，记录了已经`mint`过的地址，某地址mint成功后进行赋值
        
*   函数
    
    1.  构造函数初始化`NFT`的名称和代号，还有`Merkle Tree`的`root`
        
    2.  `mint()`函数接受地址`address`，`tokenId`和`proof`三个参数。首先验证`address`是否在白名单中，验证通过则把序号为`tokenId`的`NFT`铸造给该地址，并将它记录到`mintedAddress`。此过程中调用了`_leaf()`和`_verify()`函数
        
    3.  `_leaf()`函数计算了`Merkle Tree`的叶子地址的哈希
        
    4.  `_verify()`函数调用了`MerkleProof`库的`verify()`函数，来进行`Merkle Tree`验证

---

*Originally published on [cyptoJune](https://paragraph.com/@cyptojune/nft-merkle-tree)*
