什么是 Markle Tree

59 阅读3分钟

Markle Tree 的中文译名叫做默克尔树(或者叫哈希树), 它是一种经典的二叉树结构. 所以它的目的就是为了利用二叉树的特性提升搜索效率.

它的特点为子叶都由 hash 构建, 而子叶到根节点每一个更上层的(深度更小)节点都是基于它的子节点的hash值构建.

前置知识补充

为什么说二叉树的搜索效率比较高?

我们可以用最简单的用来做搜索的二叉树 - 二叉搜索树(Binary Search Tree) 来介绍, 它的特点是左侧节点保存的值比右侧节点的小. 一个数字集合 8 3 10 1 6 7 14 生成的二叉树示例如下:

image.png

这种特殊的结构在搜索的时候, 我们只需要判断搜索的值(target)与当前节点比较, 更小的继续搜索Tree的左侧, 更小的则搜索 Tree 的右侧. 比如我们要搜索 14 这个值在是否存在, 它的搜索逻辑为:

  1. 与根节点 8 判断, 比它大须继续搜索右侧

  2. 与右侧节点 10 比较, 比它大须继续搜索右侧

  3. 与右侧节点 14 比较, 证明 14 存在于树中

如果我们把搜索的路径图用红色block表示, 那么搜索的路径图如下所示:

image.png

如上图所示, 我们在每次搜索中都忽略了当层树的某一侧, 这使我们仅需 3 次就能获取到 14 是否存在于树中的结果. 而我们回到初始的数据集合 8 3 10 1 6 7 14 中从头开始搜索, 那么我们需要搜索足足 7 次才能做出判断. 它最终的表现类似于二分搜索, 也就是搜索算法中的剪枝(Pruning)在数据结构上的实现. 所以它提升搜索效率的本质是通过比较值的大小来构造一个特殊的二叉树.

二叉搜索树的构造逻辑是比较值的大小, 那么 Markle Tree 是怎么构造的?

Markle Tree 的构造方式

Markle tree是使用哈希值来构造二叉树的, 每个节点存的不再是数字, 而是哈希值.

简单来说, 哈希值就是一些数据输入至一个hash算法中计算所得到的:

image.png

Markle tree 的每个节点都是由它的子节点的哈希值来计算:

image.png

当我们需要搜索 h4 是否在tree中时, 我们需要从底至顶重新计算hash, 那么在此计算中即可省略 hash(h1 + h2) 侧的所有计算:

image.png

这样就实现了搜索算法的剪枝工作

Typescript 实现

现已有一些 packages 已经提供了 MarkleTree 的生成与验证,这里提供一个简单实现以供学习:

构造节点与类型

class Node {
  constructor(public value: string, public left: Node | null = null, public right: Node | null = null) {}
}

type Hash = `0x${string}`
// keccak256算法的实现
import { keccak256 } from 'somepackage';

class SimpleMerkleTree {
  // 叶结点
  leaves: Node[];
  // 层级节点
  layers: Node[][];
  // 根节点
  root: Node;

  // 一般用于
  constructor(values: Hash[]) {
    this.leaves = values.slice().map((value) => new Node(keccak256(value)));
    this.layers = [];
    this.root = this.buildTree();
  }

  buildTree() {
    let leaves = this.leaves.slice();
    let curLayers: Node[] = [];

    while (leaves.length) {
      const left = leaves.shift() as Node;
      const right = leaves.shift();

      if (right) {
        // sort pairs
        const combined = [left, right].map((node) => Buffer.from(node.value.slice(2), 'hex')).sort(Buffer.compare);

        const node = new Node(keccak256(Buffer.concat(combined)), left, right);
        curLayers.push(node);
      } else {
        const node = new Node(left.value, left);
        curLayers.push(node);
      }

      if (leaves.length === 0) {
        leaves = curLayers;
        this.layers.push(curLayers.slice());
        if (curLayers.length === 1) break;
        curLayers = [];
      }
    }

    return curLayers[0];
  }

  getProof(leafValue: Hash) {
    const tempLeaf = keccak256(leafValue);
    const leaf = this.leaves.find((leaf) => leaf.value === tempLeaf);
    if (!leaf) return [];

    const proof: string[] = [];
    let cur = leaf;
    let parent = leaf;

    // 遍历层级节点
    let i = 0;
    while (parent && i < this.layers.length - 1) {
      parent = this.layers[i].find((node) => node.left === cur || node.right === cur) as Node;

      if (parent.left && parent.left !== cur) {
        proof.push(parent.left.value);
      } else if (parent.right && parent.right !== cur) {
        proof.push(parent.right.value);
      }

      cur = parent;
      i++;
    }

    // 对齐另一侧
    proof.push(parent === this.root.left ? (this.root.right as Node).value : (this.root.left as Node).value);

    return proof as Hash[];
  }

  verify(proof: string[], target: Hash) {
    const root = this.root.value;
    let cur = keccak256(target);
    for (let i = 0; i < proof.length; i++) {
      const combined = [Buffer.from(cur.slice(2), 'hex'), Buffer.from(proof[i].slice(2), 'hex')].sort(Buffer.compare);
      cur = keccak256(Buffer.concat(combined));
    }

    return cur === root;
  }
}

相关链接

哈希树 - wiki