Michael.W谈hyperledger Fabric第23期-详细带读Fabric的源码8-peer节点账本读写的底层机制与RWSet读写集合
1 账本读写的流程
从网络角度上看,账本的读写可以归结为三个部分:
- 背书交易(peer节点的模拟交易)
- 交易排序(orderer节点引导的对模拟交易的排序)
- 验证交易(peer节点来验证排序后交易的有效性)
-
模拟交易是在背书节点执行的,最终返回了一个RWSet(读写集合)。该读写集合的本质就是告诉区块链我在模拟这笔交易的时候读取了什么数据又要写入那些数据(更新或删除都是写操作)。
-
交易排序发生在orderer节点,前面已经讲过。
-
交易验证是一个更新状态的过程,更新的就是读写集合中的写集合。
2 RWSet(读写集合)
交易读写集中分可分为三个部分:
-
读集:读取到的已提交的状态键。
注:
在其他区块链中,有个区块确认高度的概念。表示在这个高度之前区块链数据是被整个区块量网络认可的,不可再更改的。这样做的目的是为了防止分叉。
在Fabric中没有分叉的概念,所以对应的"确认高度"就是上一个区块,即上一个区块就被认定为已提交的状态。 -
写集:即将要更新的状态键值对。
注:
所谓的写操作包括写入新的状态键值对、更新已存在的状态键值对和删除状态键值对。
删除键值对并不是真的将某键值对删除,而是对该键值对做删除标记。
并且,如果在交易中多次对同一个状态键进行修改,那么写集中只会保留最后一次修改操作。所以说,交易是Fabric中的原子操作,无法再细化了。 -
版本号:由区块高度和交易ID组成的一个二元组。
注:
版本号主要是为了记录在这笔交易中读的数据所对应的版本。这和其他数据库中的版本管理很相近,它并不是在一个数据更新后将其版本号自加1,而是以区块高度+交易ID作为版本号。这样系统不需要另外开辟一个额外的字段来保存版本号,直接使用已经存在的数据经过处理作为版本号管理。
综上,可以理解为peer背书节点在执行模拟交易后会返回一个RWSet(读写集合)。之后这些读写集合会在orderer节点中进行排序。
在排序的过程中,orderer节点是不会对这些读写集合内部的数据进行检查。而真正需要进入到这些读写集合中的是交易的最后一个阶段——交易验证阶段。
3 交易的验证
3.1 交易验证的实现机制
交易的验证阶段除了常规的对交易的权限和签名验证以后,最重要的就是对排序后的读写集合进行验证。
网上我看过很多对读写集合验证过程进行分析的资料,其中我最认同的是:验证读写集合中的读集中的版本号是否跟世界状态的版本号一致。这也包括在当前这笔交易之前尚未被提交的交易,即在当前区块中排在这笔交易之前的交易。
记住一点:交易验证只考察读集,跟写集没有半毛钱关系。
举个例子:
假设该区块之前的世界状态为:
(键1,值1,版本号1),(键2,值2,版本号2),(键3,值3,版本号3),(键4,值4,版本号4)
现在有如下交易:
-
交易1:写操作,更新了
键1
和键2
,那么版本号1
和版本号2
也对应做了更新。由于没有做读操作,所以读集是没变的,该交易是有效的。由于交易被认定为有效后世界状态中的键1
和键2
对应的版本号也被更新。 -
交易2:读
键1
和更改键3
。由于此时世界状态中键1的版本号已经改变了,所以与读集中的版本号不一样,该交易无效。这样导致交易2
中的更改键3
的操作也变成无效。交易和交易之间是隔离的,互不知彼此存在。
-
交易3:写操作,更新
键2
。由于没有进行读操作,所以交易有效。 -
交易4:读
键3
,写键4
。由于交易2为无效交易,所以键3
并没有被更改,那么世界状态中对应的版本号也是没有改变的。那么交易4
中读键3
就是有效交易了。后面的写键4
由于没有读操作,所以是有效交易。
3.2 Fabric中为何要采用读写集合这种验证机制呢?
这种设计的目的是为了在交易中存储执行完交易后具体的值,而非产生交易的指令操作。
3.3 Fabric中所有记录的内容都非指令操作么
不是的。在客户端向背书节点发送交易提案的时候,提案的具体内容就是指令操作。
4 世界状态的本质
世界状态的解释:交易执行后,所有的键所对应的最新的值。
这么来看,区块链就可以认为是一个分布式的KV数据库。
一般情况下,都是通过智能合约来同区块链进行数据交互的。在Fabric的链码编写时,读写区块链有两个很重要的方法:stub.GetState()和stub.PutState()。这跟使用其他的KV数据库没什么区别。
4.1 世界状态的作用
世界状态可以提升智能合约的执行效率。
如果每次都要回到存储区块链数据的文件块中读取数据,效率是极低的。这时,世界状态就像一个缓存。每次只需要去这里读取最新的数据,并且随着区块链进行实时更新。
4.2 如果没有世界状态整个Fabric系统还能正常运行么?
答案是可以的。
世界状态是可以随时重构的。每次peer节点启动的时候,都会自动去检查自己的世界状态是否跟当前区块链保持一致。如果不一致,就会自动根据区块链为基准调整自己的世界状态。协调一致后,才代表peer节点启动成功。
现阶段的Fabric提供levelDB
和CouchDB
作为世界状态的存储引擎。除了之前我讲到的levelDB和CouchDB在功能性上表现出的不同之外,levelDB跟peer节点处于同一个进程中,而CouchDB作为一个第三方数据库自起进程。既然是第三方数据库,那么就需要通过网络通信来进行数据的交互,也增加了运维的负担。
装填数据库的存储引擎是可以动态修改的。如果第一次peer节点使用的是levelDB,那么完全可以在第二次启动的时候改为CouchDB。
5 历史状态索引
历史状态索引是一个可选模块。是否启用,完全是看智能合约中是否有一个查询历史的需求。
历史状态索引模块中只是记录了某个键值对在某个区块中的某笔交易中被改变了。而且只是单纯地记录了发生了改变,并不记录是如何被改变的。
查询历史的过程就是获得历史索引并根据这个历史索引去对应的区块中读取对应的交易。
6 区块的存储与读取:
6.1 区块的存储
区块数据以文件块的形式进行存储的,文件命名方式"blockfile_"加上六位数编号 。
现阶段的实现中,每个文件块大小为64M
,这是硬编码到代码中的,并不是以可配置的形式暴露给使用者的。如果想要更改这个数值就要修改源代码并重新编译peer节点。
所以一个账本的最大容量就是64M * 区块编号的最大值。区块编号的最大值可设置的范围是(0~999999)。
6.2 区块的读取
Fabric中实现的区块读取分为:
- 区块文件流(blockfileStream) :用于读取文件块
- 区块流(blockStream) :在一个文件块中读取区块
- 区块迭代器(blocksItr):在整个区块链上读取区块
它们之间的关系是自上而下依次递进,这个在后面的源码阅读中可以清晰的看见。
6.3 区块索引
设计的目的是为了快速定位区块。
具体实现流程:将查询条件与区块位置建立映射关系。
查询条件可以为:区块高度、区块哈希和哈希交易。
对应的索引值为:区块文件编号+偏移量+区块数据长度。即,从哪个文件开始读->从这个文件的哪里开始读->读多长的数据。
7 区块的提交
一个peer节点从orderer节点拿到排序后的数据,通过交易验证之后如何将区块中的数据写入账本的?
-
现将区块保存到文件系统中,即保存成磁盘文件。如果一个文件块还没有写满,就直接将数据追加到该文件块文件的后面。如果一个文件块已经写满,就重新创建一个文件块。当区块存储完毕后,要更新对应的区块索引对应值。
注:
即使一笔交易验证为无效交易,也会被记录到文件系统中。Fabric这样设计我觉得是为了保证每个区块哈希的可验证性。因为区块哈希是在orderer节点生成的,peer节点拿到区块数据后只是做验证和存储,但并不会去重新计算区块的哈希。如果将无效交易从区块中剔除的话,后面如果我们要重新计算区块的哈希,这个哈希值将发生变化。这就无法来判断数据是否被篡改了。 -
更新世界状态。此时无效交易是无法被更新的!
-
更新历史状态:将所有有效交易的写集分别进行组合键的组合,然后插入到历史状态的数据库中。
因为区块的提交过程中涉及到了文件的读写和数据库的读写。这些操作都是可以产生error的(go语言中的错误接口),所以每次在peer启动时,都要去检查区块链
、世界状态
和历史状态
三者之间的一致性。如果不一致,要以区块文件
为标准来进行同步。因为在区块的提交过程中,写区块文件是第一步执行的。
ps:
本人热爱图灵,热爱中本聪,热爱V神,热爱一切被梨花照过的姑娘。
以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。
同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下!
如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人