跳到主要内容

Ethers极简入门: 17. MerkleTree脚本

我最近在重新学ethers.js,巩固一下细节,也写一个WTF Ethers极简入门,供小白们使用。

推特@0xAA_Science

WTF Academy社群: 官网 wtf.academy | WTF Solidity教程 | discord | 微信群申请

所有代码和教程开源在github: github.com/WTFAcademy/WTFEthers


这一讲我们写一个利用Merkle Tree白名单铸造NFT的脚本,如果你对Merkle Tree合约不熟悉,请看WTF Solidity极简教程第37讲:Merkle Tree

Merkle Tree

Merkle Tree,也叫默克尔树或哈希树,是区块链的底层加密技术,被比特币和以太坊区块链广泛采用。Merkle Tree是一种自下而上构建的加密树,每个叶子是对应数据的哈希,而每个非叶子为它的2个子节点的哈希。

Merkle Tree

Merkle Tree允许对大型数据结构的内容进行有效和安全的验证(Merkle Proof)。对于有N个叶子结点的Merkle Tree,在已知root根值的情况下,验证某个数据是否有效(属于Merkle Tree叶子结点)只需要log(N)个数据(也叫proof),非常高效。如果数据有误,或者给的proof错误,则无法还原出root根植。下面的例子中,叶子L1Merkle proofHash 0-1Hash 1:知道这两个值,就能验证L1的值是不是在Merkle Tree的叶子中。

Merkle Proof

Merkle Tree合约简述

WTF Solidity极简教程第36讲:Merkle Tree中的MerkleTree合约利用Merkle Tree验证白名单铸造NFT。我们简单讲下这里用到的两个函数:

  1. 构造函数:初始化NFT的名称,代号,和Merkle Treeroot

  2. mint():利用Merkle Proof验证白名单地址并铸造。参数为白名单地址account,铸造的tokenId,和proof

MerkleTree.js

MerkleTree.js是构建Merkle TreeMerkle Proof的Javascript包(Github连接)。你可以用npm安装他:

npm install merkletreejs

这里,我们演示如何生成叶子数据包含4个白名单地址的Merkle Tree

  1. 创建白名单地址数组。

    import { utils } from "ethers";
    import { MerkleTree } from "merkletreejs";
    // 白名单地址
    const tokens = [
    "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
    "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
    "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db",
    "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
    ];
  2. 将数据进行keccak256哈希(与solidity使用的哈希函数匹配),创建叶子结点。

    const leaf = tokens.map(x => utils.keccak256(x))
  3. 创建Merkle Tree,哈希函数仍然选择keccak256,可选参数sortPairs: true,与Merkle Tree合约处理方式保持一致。

    const merkletree = new MerkleTree(leaf, utils.keccak256, { sortPairs: true });
  4. 获得Merkle Treeroot

    const root = merkletree.getHexRoot()
  5. 获得第0个叶子节点的proof

    const proof = merkletree.getHexProof(leaf[0]);

Merkle Tree白名单铸造NFT

这里,我们举个例子,利用MerkleTree.jsethers.js验证白名单并铸造NFT

  1. 生成Merkle Tree

    // 1. 生成merkle tree
    console.log("\n1. 生成merkle tree")
    // 白名单地址
    const tokens = [
    "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
    "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
    "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db",
    "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
    ];
    // leaf, merkletree, proof
    const leaf = tokens.map(x => utils.keccak256(x))
    const merkletree = new MerkleTree(leaf, utils.keccak256, { sortPairs: true });
    const proof = merkletree.getHexProof(leaf[0]);
    const root = merkletree.getHexRoot()
    console.log("Leaf:")
    console.log(leaf)
    console.log("\nMerkleTree:")
    console.log(merkletree.toString())
    console.log("\nProof:")
    console.log(proof)
    console.log("\nRoot:")
    console.log(root)

    生成Merkle Tree

  2. 创建provider和wallet

    // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 
    const ALCHEMY_RINKEBY_URL = 'https://eth-rinkeby.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l';
    const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_RINKEBY_URL);
    // 利用私钥和provider创建wallet对象
    const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b'
    const wallet = new ethers.Wallet(privateKey, provider)
  3. 创建合约工厂,为部署合约做准备。

    // 3. 创建合约工厂
    // NFT的abi
    const abiNFT = [
    "constructor(string memory name, string memory symbol, bytes32 merkleroot)",
    "function name() view returns (string)",
    "function symbol() view returns (string)",
    "function mint(address account, uint256 tokenId, bytes32[] calldata proof) external",
    "function ownerOf(uint256) view returns (address)",
    "function balanceOf(address) view returns (uint256)",
    ];
    // 合约字节码,在remix中,你可以在两个地方找到Bytecode
    // i. 部署面板的Bytecode按钮
    // ii. 文件面板artifact文件夹下与合约同名的json文件中
    // 里面"object"字段对应的数据就是Bytecode,挺长的,608060起始
    // "object": "608060405260646000553480156100...
    const bytecodeNFT = contractJson.default.object;
    const factoryNFT = new ethers.ContractFactory(abiNFT, bytecodeNFT, wallet);
  4. 利用contractFactory部署NFT合约

    console.log("\n2. 利用contractFactory部署NFT合约")
    // 部署合约,填入constructor的参数
    const contractNFT = await factoryNFT.deploy("WTF Merkle Tree", "WTF", root)
    console.log(`合约地址: ${contractNFT.address}`);
    // console.log("部署合约的交易详情")
    // console.log(contractNFT.deployTransaction)
    console.log("等待合约部署上链")
    await contractNFT.deployed()
    // 也可以用 contractNFT.deployTransaction.wait()
    console.log("合约已上链")

    部署Merkle Tree合约

  5. 调用mint()函数,利用merkle tree验证白名单,并给第0个地址铸造NFT。在mint成功后可以看到NFT余额变为1

    console.log("\n3. 调用mint()函数,利用merkle tree验证白名单,给第一个地址铸造NFT")
    console.log(`NFT名称: ${await contractNFT.name()}`)
    console.log(`NFT代号: ${await contractNFT.symbol()}`)
    let tx = await contractNFT.mint(tokens[0], "0", proof)
    console.log("铸造中,等待交易上链")
    await tx.wait()
    console.log(`mint成功,地址${tokens[0]} 的NFT余额: ${await contractNFT.balanceOf(tokens[0])}\n`)

    白名单铸造

用于生产环境

在生产环境使用Merkle Tree验证白名单发行NFT主要有以下步骤:

  1. 确定白名单列表。
  2. 在后端生成白名单列表的Merkle Tree
  3. 部署NFT合约,并将Merkle Treeroot保存在合约中。
  4. 用户铸造时,向后端请求地址对应的proof
  5. 用户调用mint()函数进行铸造NFT

总结

这一讲,我们简单介绍了Merkle Tree,并利用MerkleTree.jsethers.js创建、验证白名单,铸造NFT