理解Substrate数据存储的底层实现Merkle Patricia Trie

March 17, 2020
Blockchain Substrate

[TOC]

通过本文,你会了解到:

  • 区块链应用为什么使用Merkle Tree的数据结构;
  • Substrate采用的Patricia Merkle Trie的特点和应用。

Merkle Tree介绍

Merkle Tree是一种数据结构,用来验证计算机之间存储和传输数据的一致性,如果不使用这一数据结构,一致性的验证需要消耗大量的存储和网络资源,如比对计算机之间的所有数据;使用Merkle Tree,只需要比对merkle root(根节点)就可以达到相同的效果。整个过程,简单的描述如下:

  • 将数据通过哈希之后放置在叶子节点之中;
  • 将相邻两个数据的哈希值组合在一起,得出一个新的哈希值;
  • 依次类推,直到只有一个节点也就是根节点;
  • 在验证另外的计算机拥有和本机相同的数据时,只需验证其提供的根节点和自己的根节点一致即可。

Merke Tree使用了加密哈希算法来快速验证数据一致性,常用的加密哈希算法有SHA-256,SHA-3,Blake2等,它们可以做到,

  • 相同的输入有相同的输出;
  • 对任意数据可以实现快速计算;
  • 从哈希值无法推断出原信息;
  • 不会碰撞(即不同输入对应相同输出);
  • 输入即使只有很小的改变,输出也会有极大不同。

merkle_tree

在区块链应用Bitcoin网络中,存储的数据为转移Bitcoin的交易,如“Alice发送给Bob 5个比特币”,通过使用Merkle Tree,除了上面提到的验证各个节点之间的数据一致性,还可以用来快速验证一个交易是否属于某个区块。轻节点只需要下载很少的数据就可以验证交易的有效性,例如下图所示,用户要验证交易T(D)在某个区块之中,需要依赖的数据仅仅是HC, HAB, HEFGH, 和 merkle root即HABCDEFGH。

spv

Merkle Patricia Trie原理

Trie

Patria Trie也是一种树形的数据结构,也称为Prefix Tree,Radix Tree,或者简称为Trie,最早来源于英文单词 retrieve,可以发音为try,常用的使用场景包括:

  • 搜索引擎的自动补全功能;
  • IP路由等。

trie_autocomplete

Trie的特点是,某节点的key是从根节点到该节点的路径,即不同的key有相同前缀时,它们共享前缀所对应的路径。这种数据结构,可用于快速查找前缀相同的数据,内存开销较少。如以下数据及对应的trie表示为:

keyvalue
to7
tea3
ted4
ten12
A15
i11
in5
inn9

trie_wiki

Substrate使用base-16,即每个节点最多有16个子节点:

base16_trie

MPT

Merkle Patricia Trie(下面简称MPT),在Trie的基础上,给每个节点计算了一个哈希值,在Substrate中,该值通过对节点内容进行Blake2运算取得,用来索引数据库和计算merkle root。也就是说,MPT用到了两种key的类型。

一种是Trie路径所对应的key,由runtime模块的存储单元决定。使用Substrate开发的应用链,它所拥有的各个模块的存储单元会通过交易进行修改,成为链上状态(简称为state)。每个存储单元的状态都是通过键值对以trie节点的形式进行索引或者保存的,这里键值对的value是原始数据(如数值、布尔)的SCALE编码结果,并作为MPT节点内容的一部分进行保存;key是模块、存储单元等的哈希组合,且和存储数据类型紧密相关,如:

  • 单值类型(即Storage Value),它的key是Twox128(module_prefix) ++ Twox128(storage_prefix)
  • 简单映射类型(即map),可以表示一系列的键值数据,它的存储位置和map中的键相关,即Twox128(module_prefix) + Twox128(storage_prefix) + hasher(encode(map_key))
  • 链接映射类型(即linked_map),和map类似,key是Twox128(module_prefix) + Twox128(storage_prefix) + hasher(encode(map_key));它的head存储在Twox128(module) + Twox128("HeadOf" + storage_prefix)
  • 双键映射类型(即double_map),key是twox128(module_prefix) + twox128(storage_prefix) + hasher1(encode(map_key1)) + hasher2(encode(map_key2))

计算key所用到 Twox128 是一种非加密的哈希算法,计算速度非常快,但去除了一些严格的要求,如不会碰撞、很小的输入改变导致极大的输出改变等,从而无法保证安全性,适用于输入固定且数量有限的场景中。module_prefix通常是模块的实例名称;storage_prefix通常是存储单元的名称;原始的key通过SCALE编码器进行编码,再进行哈希运算,这里的哈希算法是可配置的,如果输入来源不可信如用户输入,则使用Blake2(也是默认的哈希算法),否则可以使用Twox。

另一种是数据库存储和计算merkle root使用的key,可以通过对节点内容进行哈希运算得到,在键值数据库(即RocksDB,和LevelDB相比,RocksDB有更多的性能优化和特性)中索引相应的trie节点。

Trie节点主要有三类,即叶子节点(Leaf)、有值分支节点(BranchWithValue)和无值分支节点(BranchNoValue);有一个特例,当trie本身为空的时候存在唯一的空节点(Empty)。根据类型不同,trie节点存储内容有稍许不同,通常会包含header、trie路径的部分key、children节点以及value。下面举一个具体例子。

Trie路径的key和value如下表所示

trie_kv_table

将上表的数据展示为trie的结构之后得到下图

trie_visual

图上所示内容的说明如下:

  • 这里使用不同的header值来表示不同的节点类型,即01表示叶子节点,10表示无值的分支节点,11表示有值的分支节点。
  • 为了提高存储和查找的效率,Substrate使用的MPT和基本的trie不同的是,并不是trie path key的每一位都对应一个节点,当key的连续位之间没有分叉时,节点可以直接使用该连续位。对于叶子节点,连续位可以是trie path key的最后几位;对于分支节点,连续位是从当前位开始到出现分叉位结束。
  • 之前提到过,Substrate采用了base-16的trie结构,即一个分支节点最多可以有16个子节点。

接着,我们来添加每个节点的哈希

trie_visual_with_hash

首先计算出叶子节点的哈希值,它们被上一级的分支节点所引用,用来在数据库中查找对应的节点;然后计算分支节点的哈希值,直至递归抵达根节点,这里用到的哈希算法是Blake2。

数据库的物理存储大致如下

keyvalue
Hrootencode(root_node)
H00encode(node_00)
H01encode(node_01)
H02encode(node_02)
H03encode(node_03)
H04encode(node_04)

数据库中存储的key是上面所计算的节点哈希;存储的value是节点内容的特定编码,对于节点中保存的值是对应的SCALE编码结果。

RocksDB提供了column的概念,用来存储互相隔离的数据。例如,HEADER column存储着所有的区块头;BODY column存储着所有的区块体,也就是所有的存储单元状态。

Child Trie

Substrate还提供child trie这样的数据结构,它也是一个MPT。Substrate的应用链可以有很多child trie,每个child trie有唯一的标识进行索引,它们可以彼此隔离,提升存储和查询效率。State trie或者main trie的叶子节点可以是child trie的根哈希,来保存对应child trie的状态。Child trie的一个典型应用是Substrate的contracts功能模块,每一个智能合约对应着自己唯一的child trie。

区块裁剪

区块的无限增长往往给去中心的节点造成较大的存储消耗,通常只有少数的节点需要提供过往历史数据,大部分节点在能够确保状态最终性之后,可以将不需要的区块数据删除,从而提升节点的性能。Substrate也内置了裁剪的功能,示意图如下:

pruning

在区块13中,只有node-4的值从04变为40,其哈希也跟着改变,而其它节点的数据并没有变化,所以新的区块根节点可以复用原有节点,仅仅更新发生变化的节点。假设区块13已经具有最终性,那么区块12中的node-4就可以删除掉。Substrate默认的区块生成算法是BABE或者Aura,而最终性是通过GRANDPA来决定的,在网络稳定的情况下,仅保留一定数量的最新区块是可行的。

总结

本文介绍了区块链应用必不可少Merkle Tree,以及Substrate采用的Patricia Merkle Trie的不同之处。需要说明的是,Substrate还提供了强大的cache功能,来提升读写效率。

引用

Deep Dive - Substrate Storage

Allow updating configuration of changes tries

Trie Data Structure | Insert and search

Wiki Trie

Trying to Understand Tries

Implement Trie (Prefix Tree)

Level DB - Quick Cheat Sheet

Understanding the ethereum trie

Diving into Ethereum’s world state

Merkle Tree & Merkle Root Explained

更多

Substrate官方文档:https://substrate.dev/

Parity介绍:https://www.parity.io/

Substrate源码:https://github.com/paritytech/substrate

Polkadot源码:https://github.com/paritytech/polkadot

comments powered by Disqus