分别使用默克尔树和数字签名两种方式给NFT合约添加白名单

前言

本文分别采用默克默克尔树和数字签名两种方式给nft合约添加白名单,对比两者的优缺点,本文包含了合约的开发,测试,部署全流程。

基础概念

默克尔树:也称为哈希树,是一种树形数据结构,主要用于数据验证和同步,默克尔树的特点是每个非叶子节点是其子节点的哈希值,而叶子节点存储的是数据或数据的哈希

数字签名(ECDSA):以太坊采用的数字签名双椭圆曲线数字签名算法(ECDSA);

开发

默克尔树白名单NFT合约

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
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));
    }
}
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);
    }
}

数字签名白名单NFT合约

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import "hardhat/console.sol";
contract SignatureNFT is ERC721 {
    using ECDSA for bytes32;
    using MessageHashUtils for bytes32;
    address public signer; // 签名地址
    mapping(address => bool) public mintedAddress; // 记录已经铸造的地址

    constructor(string memory _name, string memory _symbol, address _signer)
        ERC721(_name, _symbol)
    {
        signer = _signer;
    }

    // 验证签名并铸造 NFT
    function mint(address _account, uint256 _tokenId, bytes memory _signature) external {
        bytes32 messageHash = getMessageHash(_account, _tokenId);
        bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash();
        console.log(verify(ethSignedMessageHash, _signature));
        require(verify(ethSignedMessageHash, _signature), "Invalid signature");
        require(!mintedAddress[_account], "Already minted!");
        _mint(_account, _tokenId);
        mintedAddress[_account] = true; 
    }

    // 生成消息哈希
    function getMessageHash(address _account, uint256 _tokenId) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_account, _tokenId));
    }

    // 验证签名
    function verify(bytes32 _msgHash, bytes memory _signature) public view returns (bool) {
        console.log(signer);
        console.log(_msgHash.recover(_signature));
        return _msgHash.recover(_signature) == signer;
    }
}
# 编译指令
# npx hardhat compile

测试

默克尔树白名单NFT合约

const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("MerkleTreeNFT",async()=>{
    let MerkleTreeNFT;//合约
    let firstAccount//第一个账户
    let secondAccount//第二个账户;
    let addr1;//第一个账户
    let addr2;//第二个账户
    let addr3;//第三个账户
    let addr4;//第四个账户
    let Proof=[
        "0x00314e565e0574cb412563df634608d76f5c59d9f817e85966100ec1d48005c0",
        "0x7e0eefeb2d8740528b8f598997a219669f0842302d3c573e9bb7262be3387e63"
      ]//通过MerkleTree网页获取0 Proof下的数组
      let Proof1=[
        "0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9",
        "0x7e0eefeb2d8740528b8f598997a219669f0842302d3c573e9bb7262be3387e63"
      ]//通过MerkleTree网页获取1 Proof下的数组
      let Proof2=[
        "0x1ebaa930b8e9130423c183bf38b0564b0103180b7dad301013b18e59880541ae",
        "0x070e8db97b197cc0e4a1790c5e6c3667bab32d733db7f815fbe84f5824c7168d"
      ]//通过MerkleTree网页获取2 Proof下的数组
      let Proof3=[
        "0x8a3552d60a98e0ade765adddad0a2e420ca9b1eef5f326ba7ab860bb4ea72c94",
        "0x070e8db97b197cc0e4a1790c5e6c3667bab32d733db7f815fbe84f5824c7168d"
      ]//通过MerkleTree网页获取3 Proof下的数组
    beforeEach(async()=>{
        await deployments.fixture(["merkeTree"]);
        firstAccount=(await getNamedAccounts()).firstAccount;
        secondAccount=(await getNamedAccounts()).secondAccount;
        [addr1,addr2,addr3,addr4]=await  ethers.getSigners();
        const MerkleTreeDeployment = await deployments.get("MerkleTree");
        MerkleTreeNFT = await ethers.getContractAt("MerkleTree",MerkleTreeDeployment.address);//已经部署的合约交互
    })
    describe("MerkleTreeNFT测试",async()=>{
        it("验证合约",async()=>{
            //账户
            //addr1,addr2.addr3,addr4
            //白名单账号铸造
            await MerkleTreeNFT.mint(addr1.address,0,Proof)
            console.log( await MerkleTreeNFT.ownerOf(0))
            //只能铸造一次
            // await MerkleTreeNFT.mint(firstAccount,0,Proof)
            // console.log('报错', await MerkleTreeNFT.ownerOf(0))
            //白名单addr2 铸造
            await MerkleTreeNFT.mint(addr2.address,1,Proof1)
            console.log(await MerkleTreeNFT.ownerOf(1))
            //白名单addr3 铸造
            await MerkleTreeNFT.mint(addr3.address,2,Proof2)
            console.log(await MerkleTreeNFT.ownerOf(2))
            //白名单addr4 铸造
            await MerkleTreeNFT.mint(addr4.address,3,Proof3)
            console.log(await MerkleTreeNFT.ownerOf(3))
        }) 
    })
})
# 测试指令
# npx hardhat test ./test/xxx.js

数字签名白名单NFT合约

测试时铸造需要的数字签名获取

步骤如何下

  • 把需要签名的钱包地址导入到Metamask钱包中
  • 本地测试的情况使用opensea测试网
  • 打开控制台输入如下代码
  • await ethereum.enable()//链接网站
  • accunt="要导入的地址"//导入账号的地址例如0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
  • hash="生成的hash"//可以通过部署的合约中的getMessageHash方法生成
  • await ethereum.request({method:"personal_sign",params:[account,hash]})//获取数字签名铸造时使用
const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("数字签名",function(){
    let Signature;//合约地址
    let firstAccount//第一个账户
    let secondAccount//第二个账户
    //签名可以通过钱包签名,也可以通过合约签名这里主要用钱包签名
    //步骤入下
    //1.在metamask钱包导入账户
    //2.打开opensea控制台
    //3.输入ethereum.enable()//链接网站
    //4.输入account=""//导入的钱包公钥
    //5.输入hash=""//可以通过部署的合约地址获取
    //6.输入await ethereum.request({method:"personal_sign",params:[account,hash]})//获取签名
    let walletSignature="0x7c016a14819cd75ef2321cdf18f415fb54d8faf077e23b259a6d1033b530e5fb738021508e6c9e449e8c9b8f1503163ca327e518b2f6aa4b3ca9d5f9392cd3301c"
    beforeEach(async function(){
        await deployments.fixture(["SignatureNFT"]);
        firstAccount=(await getNamedAccounts()).firstAccount;
        secondAccount=(await getNamedAccounts()).secondAccount;
        const SignatureDeployment = await deployments.get("SignatureNFT");
        Signature = await ethers.getContractAt("SignatureNFT",SignatureDeployment.address);//已经部署的合约交互
       
    });
    describe("数字签名",function(){
        it("SignatureNFT",async function(){
            //获取hash 参数说明 地址,id
           const  Signaturehash=await Signature.getMessageHash(firstAccount,0);
           console.log('获取hash',Signaturehash)
           //白名单账户铸造nft 参数 地址,id,签名
            await Signature.mint(firstAccount,0,walletSignature)
            //验证该账号是否铸造成功 获取有一个nft
            console.log(await Signature.balanceOf(firstAccount))
            //再次铸造失败
             await Signature.mint(firstAccount,0,walletSignature)
            //获取有一个nft验证该账号是否铸造成功
            console.log(await Signature.balanceOf(firstAccount))
        });
    })
})
# 测试指令
# npx hardhat test ./test/xxx.js

部署

默克尔树白名单NFT合约

部署参数说明和获取: 参数4个:nft的name ,nft的符号,默克尔树的根节点,初始化Owner

关于默克尔树的根节点获取可以通过默克网页自动生成

步骤如何:

  1. 打开hardhat项目在终端中输入 npx hardhat node 可以获取20个账户
  2. 打开默克尔树网站
  3. 把第一步获取的账号的公钥的地址添到input的数字中,选择hash方法为Keccak-256 options中选择hashLeaves和sortPairs选项
  4. 点击compute,
  5. 成功后就会生成OutputRoot中就会生成Roothash(例: 0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9
module.exports=async function ({getNamedAccounts,deployments}){
    const  firstAccount= (await getNamedAccounts()).firstAccount;
    const root="0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9";//通过默克尔网页生成
    const {deploy,log} = deployments;
    const MerkeTree=await deploy("MerkleTree",{
        from:firstAccount,
        args: ["MerkeTree","MKTNFT",root],//参数 name,symble,root
        log: true,
    })
    console.log('默克尔树合约地址',MerkeTree.address)
}
module.exports.tags=["all","merkeTree"]
# 部署指令
# npx hardhat deploy

数字签名白名单NFT合约

module.exports=async function ({getNamedAccounts,deployments}){
    const  firstAccount= (await getNamedAccounts()).firstAccount;
    const {deploy,log} = deployments;
    const Signature=await deploy("SignatureNFT",{
        from:firstAccount,
        args: ["Signature","SNFT",firstAccount],//参数 name,symble,签名者地址
        log: true,
    })
    console.log('签名合约地址',Signature.address)
}
module.exports.tags=["all","SignatureNFT"]
# 部署指令
# npx hardhat deploy

区别

默克尔树

优点:Gas成本低、验证效率高、数据完整性保障;

缺点:生成和维护成本高、数据隐私性较差;

数字签名

优点:安全、灵活、易实现;

缺点:Gas 成本较高、验证效率较低、依赖于密钥管理;

总结

以上就是采用默克尔树和数字签名两种方式,实现NFT合约添加白名单的方法。两种方法各有优劣,根据情况酌情使用。

版权声明:
作者:感冒的梵高
链接:https://www.techfm.club/p/182761.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>