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

  • 木西
  • 发布于 2025-02-28 14:29
  • 阅读 287

前言本文分别采用默克默克尔树和数字签名两种方式给nft合约添加白名单,对比两者的优缺点,本文包含了合约的开发,测试,部署全流程。基础概念默克尔树:也称为哈希树,是一种树形数据结构,主要用于数据验证和同步,默克尔树的特点是每个非叶子节点是其子节点的哈希值,而叶子节点存储的是数据或数据的哈希

前言

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

基础概念

默克尔树:也称为哈希树,是一种树形数据结构,主要用于数据验证和同步,默克尔树的特点是每个非叶子节点是其子节点的哈希值,而叶子节点存储的是数据或数据的哈希<br> 数字签名(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 当通过proofleaf重建出的root与给定的root相等时,返回true,数据有效。
  • 在重建时,叶子节点对和元素对都是排序过的。 */ function verify( bytes32[] memory proof, bytes32 root, bytes32 leaf ) internal pure returns (bool) { return processProof(proof, leaf) == root; }

    /**

  • @dev Returns 通过Merkle树用leafproof计算出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合约
**测试时铸造需要的数字签名获取**&lt;br>
**步骤如何下**&lt;br>
* 把需要签名的钱包地址导入到Metamask钱包中
* 本地测试的情况使用[opensea测试网](https://testnets.opensea.io/zh-CN)
* 打开控制台输入如下代码
* 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&lt;br>
[关于默克尔树的根节点获取可以通过默克网页自动生成](https://lab.miguelmota.com/merkletreejs/example/)&lt;br>
步骤如何:
1. 打开hardhat项目在终端中输入 npx hardhat node 可以获取20个账户
2. [打开默克尔树网站](https://lab.miguelmota.com/merkletreejs/example/)
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成本低、验证效率高、数据完整性保障;&lt;br>
**缺点**:生成和维护成本高、数据隐私性较差;&lt;br>
### 数字签名
**优点**:安全、灵活、易实现;&lt;br>
**缺点**:Gas 成本较高、验证效率较低、依赖于密钥管理;&lt;br>
# 总结
以上就是采用默克尔树和数字签名两种方式,实现NFT合约添加白名单的方法。两种方法各有优劣,根据情况酌情使用。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
江湖只有他的大名,没有他的介绍。