背景
在上一篇文章"Solidity合约中签名验证的一点实践"中提到过,白名单机制一般有两种,除了签名验证的方式外,就是本文讲述的Merkle Root验证的方式。
主要做法是在服务端对白名单地址列表整体构建Merkle树,计算出树的root hash,合约只需存储这个Merkle的根哈希值就可以了。由于Merkle tree的构建,不需要任何私钥,所以安全性有很大提升,目前大多数新项目都会采用这个方法。
整体交互流程和签名验证比较相似,大致为:
- 后端预先收集所有的白名单地址,构建出完整的Merkle树
- 用户在前端网页操作发起pre mint时,弹出信息提示用户对该请求进行签名
请求发到后端,后端校验签名后,查询地址是否在白名单列表中。
- 如果确实存在白名单中,则从Merkle树查询该地址对应的Merkle proof列表,并返回给前端
- 前端调用钱包,把后端返回的proof列表作为参数传给合约pre mint方法
- 合约通过该地址和proof列表,计算出root hash,与合约中保存的root hash做比较,相同则通过校验,并且保存该地址到合约中,避免用户重复发起。
Merkle Tree
Merkle树在区块链中应用非常广泛,比如在比特币中就是用交易作为了叶子节点的数据节点,使得可以快速验证某一笔交易是否在区块中是否存在。概念参考: Merkle Tree与区块链
而在白名单场景中,叶子节点的数据节点就是一个一个的地址。
合约
同样,在第三方库OpenZeppelin中,已经实现(或者说定义)了根哈希验证的方法,用户的自定义合约里只有引入MerkleProof这个library即可。验证的源代码如下:
function verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf
) internal pure returns (bool) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];
if (computedHash <= proofElement) {
// Hash(current computed hash + current element of the proof)
computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
} else {
// Hash(current element of the proof + current computed hash)
computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
}
}
// Check if the computed hash (root) is equal to the provided root
return computedHash == root;
}
在这里我们可以看到传参分别是proof的byte32数组,root的hash值,以及leaf(实际就是用户地址)。这个方法中,通过leaf和对应Merkle节点的hash值,循环计算出根root哈希值,如果与传参root相同则验证通过。
其中需要注意的是
- 每次计算下一个hash时,都需要先比较computedHash与proofElement的大小,较小的在拼接时放在前面。由于两者都是bytes32的数据类型,而solidity中byte是无符号的,所以在服务端务必需要用相同规则生成这个merkle树,这样才能满足合约的校验方法。
- 如果节点总数为奇数,那么最后一个节点,需要和自身拼接后进行hash
使用该library的合约示例代码如下,rootHash则为预先保存在合约里的根哈希值:
using MerkleProof for bytes32[];
bytes32 public rootHash;
function merkleVerify(bytes32[] memory proof) public view returns(bool){
return proof.verify(rootHash,bytes32(uint256(uint160(msg.sender))));
}
后端
根据语言的不同,后端工作量差别较大。
如果是nodejs,则有OpenZeppelin推荐的merkletreejs库,直接使用即可,操作十分简单。可参考OpenZeppelin官方测试用例
如果是其他语言,则需要寻找是否有现成的第三方库,目前构建Merkle树的库很多,但是加入了节点间排序的比较少,而且构造的proof列表往往不符合solidity验证的需求。因此本文模仿merkletreejs的核心逻辑,结合web3j库,用java实现了初始化构建、生成proof列表,验证proof 这三个功能:
import org.web3j.crypto.Hash;
import org.web3j.utils.Numeric;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class MerkleTree {
private final List<byte[]> leaves;
private final List<List<byte[]>> layers;
public MerkleTree(List<String> leafList) {
this.leaves = leafList.stream().map(key -> Hash.sha3(key.getBytes())).collect(Collectors.toList());
this.layers = new ArrayList<>();
processLeaves(this.leaves);
}
private byte[] getRoot() {
if(this.layers.size() == 0){
return new byte[]{
};
}
return this.layers.get(this.layers.size()-1).get(0);