说明 前面讲解了使用签名验证的方式对账户进行确认,签名的过程也会消耗一定的时间,所以我们需要一种更加高效的方式来验证账户的状态,这种方式就是使用Merkle树来验证账户的状态,验证的过程也比较快,验证的方式也比较简单,只需要保存Merkle树的根节点的内容.
本篇文章主要讲解如何使用Merkle树来验证账户的状态,并且使用Merkle树来验证账户的状态,至于Merkle树的验证原理,这里就不再赘述了,感兴趣的可以自行查阅相关资料.
验证步骤
后台根据白名单地址生成一个 Merkle Tree,包括 Merkle Root。管理员将生成的 Merkle Root 设置到合约中;
前端(或者后端)根据当前的账户地址,生成一个 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" )let addresses = [ "0x978DCD67B155b3dBecd221Ec0D193f6fa7d3B8c2" , "0x41fed4790A6137083fac595e00090b2D01d012b6" , "0xFbC43c738d17F4d43627B8675A8cdC691A603BB3" , "0xBD925b9Fab6Eb9f713238Cc688A91a7f5c7Ff4c8" , "0xc6c74C251aa41FCB0De4c55fb751eec04f66774A" ] let leaves = addresses.map (addr => keccak256 (addr))let merkleTree = new MerkleTree (leaves, keccak256, {sortPairs : true })let rootHash = merkleTree.getRoot ().toString ('hex' )console .log (merkleTree.toString ())console .log (rootHash)let address = addresses[0 ]console .log (address)let hashedAddress = keccak256 (address)let proof = merkleTree.getHexProof (hashedAddress)console .log (proof)let v = merkleTree.verify (proof, hashedAddress, rootHash)console .log (v)
最终输出的结果是:
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的验证方式.
参考
https://mirror.xyz/xyyme.eth/kz25RzXxUDyWr9oqOWUF0M9g0Q0ke0vxlkhJ0ffT7kk