Hands-On Hyperledger Fabric——分布式账本存储

文章目录

  • 分布式账本存储概述
  • 账本
    • 账本编号
    • 账本数据
    • 交易模拟执行
      • 读写集
      • 状态数据
      • 历史数据

分布式账本存储概述

分布式账本技术通过在不同节点之间达成共识,记录相同的账本数据,这是区块链技术的基础。超级账本采用Endorsement/Consensus模型,模拟执行区块验证是在不同角色的节点中分开执行的。

模拟执行是并发的,这样可以提高扩展性和吞吐量:

  • 在endorse节点处模拟执行链码;
  • 在所有peer节点上验证交易并提交。

每个Peer节点上会维护多个账本,如下图所示:
Hands-On Hyperledger Fabric——分布式账本存储_第1张图片

每个Peer节点会维护四个DB,分别为:

  • 账本索引库(IdStore):存储ChainID
  • 状态数据库(StateDB): 存储world state
  • 历史数据库(HistoryDB): 存储Key的版本变化
  • 区块索引库(BlockIndex):存储Block索引

账本

超级账本包含以下元素:

  • 账本编号:快速查询存在哪些账本。
  • 账本数据:实际的区块数据存储。
  • 区块索引:快速查询区块/交易。
  • 状态数据:最新的世界状态(World State)数据。
  • 历史数据:跟踪键的历史。

账本编号

账本编号(LedgerID)的数据存储在LevelDB中,记录了有哪些账本以及其全局唯一的编号。

账本数据

账本数据(Ledger)以二进制文件的格式进行保存,每个账本数据保存在不同的目录下。账本数据的存储并没有使用数据库,而是采用的基于文件的系统。所以,它的所有操作都是通过区块文件管理器(blockfileMgr)实现的。

committer节点负责维护节点本地的账本数据,通过Gossip模块从排序服务接收到区块后,会保存区块并建立区块的索引,然后保存到数据库中。

交易模拟执行

链码是可以并行执行的,执行过程并不影响当前的状态数据库。其实现方法是在最新账本上生成一个账本数据的模拟器,模拟执行过程生成的数据会写入模拟器的writeMap中,读取的数据会写入到readMap中,然后根据writeMap和readMap生成读写集。由于每次链码执行都会生成一个新的模拟器,多个链码并行执行并不会互相影响,模拟执行的结果也不会影响到当前的状态数据库,因为生成的读写集在committer节点提交交易的时候,只有验证通过以后才会记录到账本中。

读写集

在endorse节点模拟执行交易的过程中,会生成读写集(Read-Write Set)。

  • 读集合包含了唯一键的列表,还有在模拟执行过程中交易读取的已提交键值。
  • 写集合也包含了一个唯一键的列表,还有在模拟执行过程中交易写的键值。(写集合还包含删除标记:是否删除Key)

下面是一个读写集的简单示例,版本号为了描述简单用数字表示:

<TxReadWriteSet>
  <NsReadWriteSet name="chaincode1">
    <read-set>
      
      
    read-set>
    <write-set>
      
      
      
    write-set>
  NsReadWriteSet>
<TxReadWriteSet>

committer节点根据读写集中的读集合来验证交易,根据写集合来更新键的版本和值。

在验证阶段,怎么判断交易的合法性呢?

  1. 首先committer节点会检查读集合的版本号,比较在交易中读集合里的版本号与世界状态(World State)的键的版本号一致。如果不一致则认为是不合法的。
  2. 然后,如果读写集中包含了query-info(规定查询范围),则会检查其他一些细节,不做赘述。

如果交易通过了上面的检查,那么提交节点会根据写集合来更新世界状态(World State),会遍历写集合中的每个键,更新世界状态里对应的键值和版本号。

为了便于理解来看一个例子。假设一个键值对在世界状态里用一个三元组来表示:(key,verion,value),也就是键key最新版本version的值是value。现在有5笔交易T1-T5,他们都基于同一个世界状态的快照进行模拟:

World state: (k1,1,v1), (k2,1,v2), (k3,1,v3), (k4,1,v4), (k5,1,v5)
T1 -> Write(k1, v1'), Write(k2, v2')
T2 -> Read(k1), Write(k3, v3')
T3 -> Write(k2, v2'')
T4 -> Write(k2, v2'''), read(k2)
T5 -> Write(k6, v6'), read(k5)

假设按照T1到T5的顺序依次进行排序:

  • T1检查通过。因为没有任何读操作。交易会更新原本的(k1,1,v1), (k2,1,v2)(k1,2,v1'), (k2,2,v2')
  • T2检查失败。因为交易需要读取的键k1在前面一个交易T1中被修改了。
  • T3检查通过。因为没有任何读操作。交易会更新为(k2,3,v2'')(多次写以最后一次为准)。
  • T4检查失败。因为需要读取的k2被修改过了。
  • T5检查通过。因为需要读取的k5没有被修改过。

个人猜测:committer节点接受排序服务的区块和读写集,如果交易验证不通过就不会被记录到区块文件中,做一个验证能够防止“双花”。尽管排序服务会对交易进行一个排序,但是只能保障应用程序提交交易的顺序,并不能保障没有恶意提交交易的存在。

状态数据

状态数据记录的是交易执行的结果,最新的状态代表了channel上所有键的最新值,所以称为World State。为了提高链码执行的效率,所有键的最新值都存储到状态数据库中。

对于状态数据库本身插件化的设计,目前支持LevelDB和CouchDB。LevelDB和CouchDB都支持基本的链码操作,比如获取和设置键值,基于键进行查询等等。

LevelDB
Fabric默认的数据库,采用C++编写的高性能嵌入式数据库。其基本操作是基于键值对的,应该是Nosql数据库。
CouchDB
文档型数据库,提供Restful的API操作。CouchDB中的文档是无模式的,并不要求文档具有某种特定的结构,支持JSON和字节数组的操作,可以支持复杂的查询。

历史数据

历史数据记录了每个状态数据的历史信息,可用于区块提交的恢复。

区块的提交过程分为3个步骤:

  1. 先保存区块到文件存储的账本数据中。
  2. 更新状态数据。
  3. 更新历史信息数据。

你可能感兴趣的:(Fabric)