默克尔树javascript实现

前言

本文主要目标是默克尔树的代码实现,基于内容完整性考虑,本文会简要说明默克尔树的基本概念及使用场景。关于默克尔树更详细的理论描述见之前的文章——《
以太坊数据结构之Merkle、MPT(Merkle Patricia Tree)
》。本文力求以最简单的代码还原默克尔树的原理,以期读者可以更深一步的理解默克尔树。

基本概念

默克尔树(Merkle Tree)是一种树状数据结构,由一个根节点和一系列的叶子节点组成。这种树的特殊之处在于,根节点的值是由叶子节点通过一系列哈希运算得到的。默克尔树的设计目的是为了有效验证大型数据集的完整性和一致性。
总的来说,默克尔树通过逐级哈希的方式提供了高效的数据完整性验证机制,广泛应用于保障数据的安全性和可信任性。

使用场景:

  • 区块链: 一个典型的应用是在区块链中用于确保交易的完整性。每个区块包含交易的默克尔树根哈希。这使得用户可以通过验证这个根哈希来确认特定的交易是否包含在区块中,而无需下载整个区块。

  • 文件系统和数据存储: 用于快速验证大型数据集的完整性。例如,在分布式文件系统中,默克尔树可以帮助验证文件的完整性,而无需下载整个文件。

  • P2P网络: 在点对点网络中,特别是分布式文件共享系统中,默克尔树可用于验证下载文件的一部分是否正确。

  • 版本控制: 一些分布式版本控制系统使用默克尔树来管理代码库的版本。

  • 加密证书: 在加密学中,默克尔树可用于构建数字证书的完整性证明,确保证书的任何修改都能被检测到。

结构示例

为了相关逻辑不至于太过抽象,请结合下面的图示来理解代码。

              Root
           /        \
     ABCDEFGH        IJ
     /      \         |
   ABCD     EFGH     IJ
   / \      /  \      |
 AB   CD   EF   GH   IJ
 / \  / \  / \  / \  / \      
A  B  C D  E F  G H  I J

在以太坊中默克尔树的每个叶子节点对应一笔交易的hash值,叶子节点两两一对将其值拼接在一起(如上图A、B),对拼接后的字符串计算hash(AB,为了直观此处以两个字符拼接的形式表示其哈希值),将计算出来的值作为叶子节点的父节点,以些类推,直到生成根节点。

代码实现

class MerkleTree {
    /*
     构造函数,接收叶子节点和拼接函数作为参数
     注:为了调试方便此处以参数的形式传入函数,比如你可以传递简单的字符串拼接函数,在以太坊中默认是哈希函数(keccak256)
     */
    constructor(leaves, concat) {
       this.leaves = leaves;  // 叶子节点数组
       this.concat = concat;  // 拼接函数
    }

    // 获取默克尔树的根节点
    getRoot() {
        let tmpArr = this.helper(this.leaves);

        // 循环构建树直到根节点
        while (tmpArr.length > 1){
            tmpArr = this.helper(tmpArr);
        }

        if (tmpArr.length > 0){
            return tmpArr[0];
        } else {
            return null;
        }
    }

    // 辅助函数,用于构建树的一层
    helper(inArr){
        let result = [];
        let len = inArr.length;
        for (let i = 0; i < len - len % 2; i += 2){
            result.push(this.concat(inArr[i], inArr[i + 1]));
        }
        if (len % 2){
            result.push(inArr[len - 1]);
        }
        return result;
    }

    // 获取指定索引的叶子节点的证明路径
    getProof(index){
        let result = [];
        let tmpArr = this.leaves;

        let len = tmpArr.length;
        while (len > 1){
            if (index % 2){
                if (index > 0){
                    result.push({data: tmpArr[index - 1], left: true});  // 记录左侧节点
                }
            } else {
               if (index < len - 1) {
                   result.push({ data: tmpArr[index + 1], left: false });  // 记录右侧节点
              }
            }
            index = Math.floor(index / 2);
            tmpArr = this.helper(tmpArr);
            len = tmpArr.length;
        }
        
        return result;
    }
}

函数说明

  1. helper
    辅助函数,用于返回当前层的计算结果,其结果作为当前层的上一层。

  2. getRoot
    用于计算默克尔树的树根,节点两两分组并计算hash,需要注意的是当节点个数为奇数时不对最后单独的节点计算hash。借助helper函数一层层向上计算,直到最后只剩一个节点即根节点。

  3. getProof
    如前所述,默克尔树只需要一些数据片段即可验证数据的完整性,此函数用于生成这些数据片段。如下图所示,如果需要验证C节点,那么只需要[D, AB, E]这三个节点即可。

     Root		---第一层 节点:[Root]
    /   \
  ABCD    E  	---第二层 节点:[ABCD,E]
  /  \    |
AB   CD   E		---第三层 节点:[AB,CD,E]
/ \  / \  |
A B  C D  E		---第四层 节点:[A,B,C,D,E]

该函数的参数是需要被证明的数据在叶子节点中的索引。如上图C节点的索引是2。
注:为了保证哈希计算的一致,两个子节点数据的顺序尤其重要,因此我们以左右来区分子节点。 我们以如下格式来表示验证数据所需的每个节点,如下是要验证C节点时需要的数据片段

[
  { data: 'D', left: false }, 
  { data: 'AB', left: true },
  { data: 'E', left: false }
]

理解如下两点尤其重要:

  • 假如我们把树中某一层的节点从左往右存入数组,由于是一颗二叉树,所以索引为偶数的节点是左节点,索引为奇数的节点是右节点。
  • 由于节点是两两组合产生父节点,因此某节点的父节点在其所在层的索引为孩子节点索引的一半(向下取整),如D节点在第四层的索引为3,则其父节点CD在第三层的索引为1(Math.floor(3/2))。

验证节点

验证的逻辑比较简单,我们只需根据待验证的数据与getProof返回的数据计算出默尔树根,然后将计算结果与正确结果比较即可。

/**
 * 
 * @param {*} node  //待验证数据
 * @param {*} proof //验证数据必要的数据片段(getProof函数返回值)
 * @param {*} root  //正确的hash根
 * @param {*} concat //节点的操作函数,同MerkleTree 类构造函数的第二个参数相同
 * @returns 
 */
function verifyProof(node, proof, root, concat) {
    let tmp = node;
    for (let i = 0; i < proof.length; i++){
       tmp = proof[i].left ? concat(proof[i].data, tmp) : concat(tmp, proof[i].data);
    }

    return tmp === root;
}

验证

const leaves = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'];
const root = "Hash(Hash(Hash(Hash(A + B) + Hash(C + D)) + Hash(Hash(E + F) + Hash(G + H))) + Hash(Hash(I + J) + K))";

const concat = (a, b) => `Hash(${a} + ${b})`;
const tree = new MerkleTree(leaves, concat);

console.log(tree.getRoot() === root);

leaves.forEach((item, idx) => {
    let proof = tree.getProof(idx);
    console.log(`Index ${idx}-${item} proof=>${JSON.stringify(proof)}`);
    console.log(verifyProof(proof, item, root, concat))
})

执行结果1执行结果2

这里有一个项目简单模拟了数字货币轻钱包交易的验证过程。

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值