Cover photo

默克尔树在NFT白名单中的应用

五月一号早上, 猴地开始公售, ethereum主网的Gasprice 维持6000gwei 2个半小时直至Mint完毕. 猴地在mintland()方法中传入两个参数, 第一个是mint的个数, 第二个是 merkleProof. 这里的merkleProof 就是默克尔树的重要元素之一.

什么是默克尔树

Merkle 树是一种用于密码学的树状结构。默克尔树有任意数量的节点,称为叶子(Leaf node)。这些是将被散列的数据值,在猴地例子中是KYC用户的地址。每对散列将再次进行hash,然后对这些散列进行hash,直到只剩下一个节点: 根节点(Root)。这确保节点中的任何更改都将反映到最终的根节点中。这使得检查值是否正确变得相当容易。

简单的默克尔树
简单的默克尔树

默克尔树最主要的应用是 验证某个节点(元素)是否在集合中. 讲人话就是:

默克尔树可以告诉你你要验证的元素对不对, 至于如果错了那它本该是什么这无可奉告!

无可奉告
无可奉告

具体要怎么做?

我们由两部分工作要做:

  1. 创建默克尔树, 拿到merkleRoot和对应地址的Proof, 其中merkleRoot上传到智能合约

  2. 使用地址和对应的Proof调用合约去验证

创建merkleRoot

我们使用javascript来创建merkleRoot, 主要使用两个库 Keccak256(Solidity 默认的hash函数) 和MerkleProofJS.

首先创建一个MerkleTree来保存我们要写的内容, 然后进入文件夹添加JS库

yarn add merkletreejs keccak

我们应该做的第一件事是创建一个列入白名单的以太坊地址列表,这里我添加了一个包含 几个随机地址(包括“0x”)的列表,并将它们作为字符串放入一个数组中。

然后我们需要使用 keccak256 对它们进行哈希处理来创建我们的叶子。完成所有叶子后,我们可以创建 merkleTree 并使用 MerkleTreeJS 获取merkleRoot。

代码如下:

import { MerkleTree } from 'merkletreejs'
import keccak256 from 'keccak256'

const whitelistAddress = ['0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 
'0x....', '0x....']

const leafNodes = whitelistAddress.map((addr) => keccak256(addr))
const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true })
const rootHash = merkleTree.getHexRoot()
console.log('merkleTree', merkleTree.toString())
console.log('rootHash', merkleTree.getHexRoot())

这里拿到了merkleRoot, 稍后可以就可以把它上传到智能合约中用以验证.

另外我们还需要拿到用户的Proof作为验证的凭据:

const proof = merkleTree.getHexProof(keccak256('0x5B38Da6a701c568545dCfcB03FcB875f56beddC4'))
console.log('proof', proof)
// 这里可以验证proof是否正确
const v = merkleTree.verify(proof, keccak256('0x5B38Da6a701c568545dCfcB03FcB875f56beddC4'), rootHash)
console.log(v)

关于proof的分发, 可以放到服务器上用户通过api来获取, 也可以写到前端页面里面来生成. 两种方法各有利弊: 前者在mint的时候可能会被攻击或者流量过大导致接口瘫痪, 但是后者会暴露merleTree的生成方法, 科学家可以查看前端源码拿到leafNodes 进而在mint之前就拿到proof获取抢跑优势.

验证

OpenZeppelin 实现并开源了merkleProof 验证,我们可以使用它添加需要的功能,以便在我们自己的合约中轻松检查证明。我们只需要创建一个根节, 实现一个验证方法, 另外为了方便后续修改Root, 实现一个修改Root的方法, 这个方法一定要限制调用权限。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MerkleTest is Ownable {
bytes32 private root;

function isWhiteList(address _address, bytes32[] calldata _merkleProof) public view returns (bool){
        bytes32 leaf = keccak256(abi.encodePacked(_address));
        require(MerkleProof.verify(_merkleProof, root, leaf), "Incorrect proof");
        return true; // Or you can mint tokens here
    }
function setMercleRoot (bytes32 _merkleRoot) public onlyOwner {              
    merkleRoot = _merkleRoot;
}
  function whitelistMint(uint256 amount, bytes32[] calldata _merkleProof) public payable {
  isWhiteList(msg.sender, _merkleProof)
  // mint
...
  }
}

在白名单发售之前只需要调用setMercleRoot() 传入最终的merkleRoot, 用户在mint的时候传入Proof即可验证.

写在最后

之前有见不少智能合约依然通过调用合约传入地址并在合约中保存的方式来发放白名单, 这种是非常浪费Gas的(当然也可能项目方不差钱). 默克尔树的形式极大的简化了白名单的分发, 验证甚至增删.

另外以上只是默克尔树使用场景的冰山一角, 它在区块链中使用非常广泛可谓是区块链的基石.例如零知识证明和P2P文件存储甚至是区块头部的组成部分.

最后感谢OpenZeppelin 和其他开源社区对Web3的无私贡献, 让我们开发者可以站在巨人的肩膀上!

post image