NFT使用Merkle Tree校验

说明

前面讲解了使用签名验证的方式对账户进行确认,签名的过程也会消耗一定的时间,所以我们需要一种更加高效的方式来验证账户的状态,这种方式就是使用Merkle树来验证账户的状态,验证的过程也比较快,验证的方式也比较简单,只需要保存Merkle树的根节点的内容.

本篇文章主要讲解如何使用Merkle树来验证账户的状态,并且使用Merkle树来验证账户的状态,至于Merkle树的验证原理,这里就不再赘述了,感兴趣的可以自行查阅相关资料.

验证步骤

  1. 后台根据白名单地址生成一个 Merkle Tree,包括 Merkle Root。管理员将生成的 Merkle Root 设置到合约中;
  2. 前端(或者后端)根据当前的账户地址,生成一个 Merkle Proof。将 Proof 作为参数传入合约中,与 msg.sender(就是前面说的账户地址)和之前设置的 Merkle Root 进行校验.

合约Merkle树验证示例

Openzeppelin 的现成合约库中已经存在通过Merkle树封装好的方法,具体如下:

1
2
3
4
5
6
7
function verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf
) internal pure returns (bool) {
return processProof(proof, leaf) == root;
}
  • proof,由链下生成传入,每个用户的 proof 都不同,本质上这个内容就是账户地址对应的Merkle树的路径
  • root, Merkle树的根节点的内容
  • leaf, 对账户地址(account)进行hash(keccak256)运算后的值

调用verify方法也很简单:

1
2
3
4
function verify(bytes32[] calldata _merkleProof) public view returns (bool) {
bytes32 leaf = keccak256(abi.encode(msg.sender));
return MerkleProof.verify(_merkleProof, merkleRoot, leaf);
}

verify方法函数唯一需要接受的就是_merkleProof(即账户地址对应的Merkle树的路径),通常是由链下(前端或者后端)计算好,然后调用MerkleProof合约中的verify方法进行验证.

实例展示

我们先使用JS代码生成Merkle树,然后使用Solidity代码验证Merkle树的根节点和路径是否正确.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const {MerkleTree} = require("merkletreejs")
const keccak256 = require("keccak256")

// 随意选择的5个地址
let addresses = [
"0x978DCD67B155b3dBecd221Ec0D193f6fa7d3B8c2",
"0x41fed4790A6137083fac595e00090b2D01d012b6",
"0xFbC43c738d17F4d43627B8675A8cdC691A603BB3",
"0xBD925b9Fab6Eb9f713238Cc688A91a7f5c7Ff4c8",
"0xc6c74C251aa41FCB0De4c55fb751eec04f66774A"
]

// 将地址进行hash运算
let leaves = addresses.map(addr => keccak256(addr))

// 生成Merkle树
let merkleTree = new MerkleTree(leaves, keccak256, {sortPairs: true})
let rootHash = merkleTree.getRoot().toString('hex')

// 输出Merkle树的根节点
console.log(merkleTree.toString())
console.log(rootHash)

// 验证某个地址是否在Merkle树中,并且输出该地址对应的Merkle树路径
// 本例中我们验证第一个地址
let address = addresses[0]
console.log(address)
let hashedAddress = keccak256(address)
let proof = merkleTree.getHexProof(hashedAddress)
console.log(proof)
// 使用JS代码进行验证
let v = merkleTree.verify(proof, hashedAddress, rootHash)
console.log(v) // returns true

最终输出的结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
└─ f4a9bce646f604898fa163b443d01b9b351de62b970293ff7c9947acf11e4691
├─ a94732b89c03a5a8976793835bd51c713e3f1f042314a3d5157255f4b874ce52
│ ├─ fe20465a5ecef34651fecf91346dc69b246b59ac2248a9a5bdd005ea20564f32
│ │ ├─ 1906fd67ae42698f12831e8e93fd6ab793358ab706bfca34a450432d6496efff
│ │ └─ 6929ccf827f8d7af9512188816a37e610191f3e71d2b9307b6dec5dd7c2c2bb0
│ └─ 23a80b7e8c1ef837ab1f02a0d7ba5518ad7bddfa64d16422b607bbfd8054fd8f
│ ├─ 7d7661839d78f58a940c57ed6057794b4d9f35e0ae319d22d3d852f6532758ba
│ └─ c3709d5a1c2cb8dd5c554c10f8ec0b80e8377d0dc5bade509a7217512d96c621
└─ 4c1de82b8f9d8cc8ce11e1951120eb344673394c6af00c4164526b297d7eb01e
└─ 4c1de82b8f9d8cc8ce11e1951120eb344673394c6af00c4164526b297d7eb01e
└─ 4c1de82b8f9d8cc8ce11e1951120eb344673394c6af00c4164526b297d7eb01e

f4a9bce646f604898fa163b443d01b9b351de62b970293ff7c9947acf11e4691
0x978DCD67B155b3dBecd221Ec0D193f6fa7d3B8c2
[
'0x6929ccf827f8d7af9512188816a37e610191f3e71d2b9307b6dec5dd7c2c2bb0',
'0x23a80b7e8c1ef837ab1f02a0d7ba5518ad7bddfa64d16422b607bbfd8054fd8f',
'0x4c1de82b8f9d8cc8ce11e1951120eb344673394c6af00c4164526b297d7eb01e'
]
true

最终的结果是true.说明我们的Merkle树的生成和验证都没有问题.接下来,使用solidity尝试验证

合约代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/TestSig.sol";
import "../src/TestMf.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract TestSigTest is Test {
bytes32[] public merkleProofdata;
function setUp() public {
merkleProofdata.push(0x6929ccf827f8d7af9512188816a37e610191f3e71d2b9307b6dec5dd7c2c2bb0);
merkleProofdata.push(0x23a80b7e8c1ef837ab1f02a0d7ba5518ad7bddfa64d16422b607bbfd8054fd8f);
merkleProofdata.push( 0x4c1de82b8f9d8cc8ce11e1951120eb344673394c6af00c4164526b297d7eb01e);
}

function testrecoverSigner() public {
// 对应的是我们测试的第一个账户地址,并对其进行了hash运算
address caller = 0x978DCD67B155b3dBecd221Ec0D193f6fa7d3B8c2;
bytes32 leaf = keccak256(abi.encodePacked(caller));

// Merkle树的根节点
bytes32 merkleRoot = 0xf4a9bce646f604898fa163b443d01b9b351de62b970293ff7c9947acf11e4691;

// 调用openzeppelin的MerkleProof库进行验证
bool result = MerkleProof.verify(merkleProofdata, merkleRoot, leaf);
console2.log(result);
}
}

最终输出的结果是true,说明solidity验证代码也是正确的.

实际应用

Cartoons:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function whitelistMint(bytes32[] calldata _proof, uint256 _amt) external payable nonReentrant {
require(totalSupply() + _amt <= MAX_SUPPLY - totalReserved, "Mint Amount Exceeds Total Allowed Mints");
require(msg.sender == tx.origin, "Minting from Contract not Allowed");
require(isWhitelistActive, "Cartoons Whitelist Mint Not Active");
uint64 newClaimTotal = _getAux(msg.sender) + uint64(_amt);
require(newClaimTotal <= rootMintAmt, "Requested Claim Amount Invalid");
require(itemPrice * _amt == msg.value, "Incorrect Payment");
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(MerkleProof.verify(_proof,root,leaf), "Invalid Proof/Root/Leaf");

_setAux(msg.sender, newClaimTotal);

_safeMint(msg.sender, _amt);
}

其中校验额核心代码是:MerkleProof.verify(_proof,root,leaf) ,采用的是merkleTree的验证方式.

tastybonesxyz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function mintFreeWithBone(
uint256 boneTokenId,
bytes32[] calldata boneProof) external isSecured(1) {

uint256 boneCount = approvingBoneContract.balanceOf(msg.sender);
bool bonesOfOwner = findBonesOfOwner(msg.sender, boneTokenId, boneCount);

require(mintedFreeMint + 1 <= maxFreeMintSupply, "EXCEEDS_FREE_MINT_SUPPLY" );
require(mintedTBforFreeMintAddress[msg.sender] + 1 <= maxMintPerAccount,"ALREADY_MINTED_MAX_2");
require(mintedTBforPresale[msg.sender] + 1 <= maxMintPerAccount, "EXCEEDS_MAX_PRESALE_MINT" );
require(totalSupply() + 1 <= maxTastyBones, "EXCEEDS_MAX_SUPPLY" );
require(MerkleProof.verify(boneProof, freeMintBoneMerkleRoot, keccak256(abi.encodePacked(msg.sender))), "YOU_ARE_NOT_WHITELISTED_TO_MINT_FREE_WBONE");
require(bonesOfOwner,"USER_DO_NOT_OWN_A_BONE");
require(boneCount > 0, "NO_BONE_PASS");
require(!mintedTBforFreeMintBone[boneTokenId], "BONE_ALREADY_USED_FOR_MINTING");

mintedTBforFreeMintAddress[msg.sender] += 1;
addressBlockBought[msg.sender] = block.timestamp;
mintedTBforFreeMintBone[boneTokenId] = true;
mintedFreeMint += 1;
_safeMint(msg.sender, 1);
}

其中的关键代码是: require(MerkleProof.verify(boneProof, freeMintBoneMerkleRoot, keccak256(abi.encodePacked(msg.sender))), "YOU_ARE_NOT_WHITELISTED_TO_MINT_FREE_WBONE");, 采用的是merkleTree的验证方式.

参考

  1. https://mirror.xyz/xyyme.eth/kz25RzXxUDyWr9oqOWUF0M9g0Q0ke0vxlkhJ0ffT7kk
文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/09/18/nft-verify-with-merkle/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow