【区块链】Hyperledger Fabric中的交易数据存储详解(学习笔记)

参考书籍:《Hyperledger Fabric菜鸟进阶攻略》—— 黎跃春等编著

账本数据

分类账本保存着所有交易变化的记录,具有有序和防篡改的特点。每一次交易,链码需要将数据变化记录在账本中,需要记录的数据称为状态,以键值对(key-value)的形式进行存储。

Fabric中的账本由两个不同但相关的部分组成。

  • 世界状态
  • 区块链

世界状态在Fabric中以键值对的方式保存一组分类账本数据状态的最新值。保存世界状态的实际上是一个NoSQL数据库,以便对状态的存储及检索;可以使应用程序无须遍历整个事务日志而快速获取当前账本的最新值。其value可以是一个简单的值,也可以由一组键值对组成的复杂数据组成。

每个世界状态有一个版本号,起始版本号值为0。每次对状态进行更改时,版本号会递增。

区块链是一个记录交易日志的文件系统,它由哈希值连接的N个区块构造而成;每个区块包含一系列的多个有序的交易。区块头包含了本区快所记录的交易的哈希值。以这种方式,账本中所有交易都被有序地并以加密的形式连接在一起。

数据存储

区块链是以文件的形式进行存储的,各区块文件默认以blockfile_为文件前缀,后面以6位数字命名,起始数字默认位000000,如有新文件则每次递增1。

区块链文件默认存储在chains文件夹,该文件夹包含两个子目录:

  • 保存区块链文件的chains目录

    ​ 该目录下以通道文件目录区分各个账本,各个peer节点对于它所属的每个通道都会保存一份该通道的账本副本。

  • 使用LevelDB实现保存索引信息的index目录

orderer节点仅会保存一份账本,不包括状态数据库及历史索引数据,这些由peer节点进行维护。

在peer节点中,除了存储一份账本外,还需要维护状态数据库、历史数据库、区块索引这些内容。

  • 状态数据库

    存储交易日志中所有Key的最新值(世界状态),默认数据库使用LeverDB(可选CouchDB)。

  • 历史数据库

    以LevelDB数据库作为数据存储载体,存储区块中有效交易相关的Key,而不存储Value。

  • idStore

    存储当前Peer节点加入的所有ledgerID(chainID/channelID)(通道ID),且保证账本编号唯一性。

读写集

在模拟执行交易后,背书节点会生成读写集(Read-Write Set),读集(Read Set)包含了交易在模拟执行期间读取的唯一key、对应已提交的值以及提交Version的列表。

写集(Write Set)包含一个唯一键列表及交易写入的新值。即Key-Value(要写入的新值)。

如果交易读取指定Key的值,则只会返回已提交的状态值Value,而不能读取同一交易中修改但未提交的值。

如果交易执行的是删除操作,则在写集中为该Key设置一个删除标记。如果在一个交易中对同一个key多次进行更改,则仅保留最后更改的值(最新值)。

如果交易执行的是范围查询,则范围查询及其结果将添加到读写集中,使用query-info来表示。

读写集示例:
在这里插入图片描述

交易验证和世界状态更新

Peer中的commiter角色节点使用读写集的读集部分来进行交易的有效性检查,写集部分更新受影响的Key的版本号和值。在验证时,使用读集中的每个Key的版本号与状态数据库中的世界状态进行比较,如果匹配,则认为此交易有效。

如果读写集中包含一个或多个查询信息,则执行额外的验证。该验证确保在此批量查询的结果范围内没有Key被新增、删除或更改。

也就是说,如果在验证期间重新执行任何的范围查询,则结果应该与交易在模拟执行时得到的结果相同。此验证确保交易在提交时出现幻读则会被认为无效。

幻读:是当事务不是独立执行时发生的现象。例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中全部数据行。同时,第二个事务在表中插入了一行新数据。那么就会发生操作第一个事务的用户发现表中还有没有修改的数据行。

比如:事务A读取表中工资为5000的员工有10个人,此时,事务B插入了一条工资为5000的员工记录,则事务A再次查询时,记录变为11人。

如果交易通过了有效性检查,则commiter角色节点使用写集来更新世界状态。在更新期间,对于写集中存在的每个key,世界状态中对应的Value与版本号都会得到更新。

模拟和验证示例

假设World State由元组(k,ver,val)表示。有5个交易,分别为T1、T2、T3、T4、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’)

world state:(k1,2,v1’)(k2,2,v2’)(k3,1,v3)(k4,1,v4)(k5,1,v5)

由于T1执行的写操作,直接更新。

②执行T2:read(k1) write(k3,v3’)

T2需要进行有效性验证,由于k1的值在T1中修改了,所以T2验证不通过。

world state:(k1,2,v1’)(k2,2,v2‘)(k3,1,v3)(k4,1,v4)(k5,1,v5)

③执行T3:write(k2,v2’’)

交易验证成功,因为没有读操作,世界状态进行更新。

world state:(k1,2,v1’)(k2,3,v2‘’)(k3,1,v3)(k4,1,v4)(k5,1,v5)

④执行T4:write(k2,v2’’’) read(k2)

交易验证失败,因为k2的值在之前被修改了

world state:(k1,2,v1’)(k2,3,v2‘’)(k3,1,v3)(k4,1,v4)(k5,1,v5)

⑤执行T5:write(k6,v6) read(k5)

交易验证成功。因为k5的值没有被修改过

world state:(k1,2,v1’)(k2,3,v2‘’)(k3,1,v3)(k4,1,v4)(k5,1,v5)(k6,1,v6)

总结:客户端将交易提案发送给背书节点后,背书节点模拟执行交易并生成读写集。其中,读集包含了交易在模拟执行期间读取的唯一key、对应已提交的值以及提交Version的列表;写集(Write Set)包含一个唯一键列表及交易写入的新值。Peer节点中的commiter角色根据读写集的读集部分进行交易有效性的验证。

以1.3的例子详细解释为什么有的交易没有通过有效性验证。

对于上述一组交易,交易提案提交时世界状态中key对应的版本version均为1。T1交易修改了k1的值,则版本号加1,变为2,值也相应改变。验证T2时,读写集包含读操作,需要将读集中的每个key版本号(k1)与世界状态里的版本号进行匹配。读集建立时k1的版本号为1,此时世界状态里k1的版本号为2,二者不匹配,验证不通过。

猜你喜欢

转载自blog.csdn.net/weixin_39006917/article/details/108348636