深度学习
状态数据(State Database)记录的是交易执行的结果,最新的状态代表了通道(Channel)上所有键的最新值,所以又称为“世界状态”。链码调用根据当前状态数 据执行交易。为了提高链码执行的效率,所有键的最新值都存储在状态数据库中。状态数据库只是区块链交易日志中的索引视图,因此可以随时根据区块链重新生 成。状态数据库在Peer节点启动时自动恢复,重新构建完成后才接受新的交易。
对于状态数据库本身插件化的设计,目前支持LevelDB和CouchDB。LevelDB和CouchDB都支持基本的链码操作,比如获取和设置键值,基于键进行查询等。
·LevelDB(默认的KV数据库):支持键的查询、组合键的查询、键范围查询。
·CouchDB(可选):支持键的查询、组合键的查询,还有复杂的查询。
不同账本的状态数据库存放在不同的目录下,同一个账本的数据是存放在一起的,不同链码的数据是按链码编号 (chaincodeID)作为命名空间(Namespace)来划分数据的。命名空间在生成组合键(compositeKey)的时候作为组合键的前 缀,分隔符可以自定义(修改源码),默认的分隔符是"0x00"。
状态数据库的基本操作是基于键值对的管理的。某个组合键k,在指定版本ver的值可以用一个三元组: (k,ver,val)表示,存储和读取数据都是版本化的。读取状态数据的时候不能指定版本,读取到的状态数据是某个时刻最新的版本,返回的数据包含版本 和数据两个部分,版本的实现用交易的高度来表示的,它是由区块编号和交易编号组成的二元组。数据结构定义如下:
type VersionedValue struct {
Value []byte // 数据
Version *version.Height // 版本
}
type Height struct {
BlockNum uint64 // 区块编号
TxNum uint64 // 交易编号
}
基本区块数据的读取也是通过键来查询的,有3种方式:查询单个键的数据、查询多个键的数据、查询一个范围内的数据。如果采用的是CouchDB方式,还可支持某些字段的条件查询,详细的内容下面的章节会有介绍。
节点验证完数据后会披露并进行更新。这可以同时包含不同链码上的数据,同时用账本编号(chaincodeID)作为命名空间分割。相同链码的数据是由不同的键值对组成的字典(map)。批量更新的数据各自有不同的版本,在更新同一批数据的同时会更新世界状态交易的高度。
状态数据库支持如下的功能。
1)根据命名空间和键获取状态数据。
·获取单个键的数据:GetState
·获取多个键的数据:GetStateMultipleKeys
·获取一个范围内的查询数据:GetStateRangeScanIterator
2)根据条件查询获取数据:ExecuteQuery。
3)更新状态数据:ApplyUpdates,可批量更新。
4)获取最新交易高度:GetLatestSavePoint。
5)数据库操作。
·打开数据库:Open
·关闭数据库:Close
5.6.1 LevelDB
LevelDB是默认的状态数据库。LevelDB是采用C++编写的一种高性能嵌入式数据库,没有独立的数据库进程,占用资源少,速度快。它有如下一些特点。
1)键和值可以是任意的字节数组。
2)数据是按键排序后存储的。
3)可以自定义排序方法。
4)基本的操作是基于键的:
·Put(key,value);
·Get(key);
·Delete(key)。
5)支持批量修改的原子操作。
6)支持创建快照。
7)支持对数据前向和后向的迭代操作。
8)数据采用Snappy压缩。
超级账本基于https://github.com/syndtr/goleveldb实 现对LevelDB数据库的操作。状态值的存储和获取都是基本的键值操作,键是包含了链码编号(chaincodeID)的组合键,值是包含了版本信息和 状态值的序列化结果。由于LevelDB不支持复杂的查询,所以使用LevelDB作为状态数据库也不支持条件查询接口:ExecuteQuery。交易 高度是一个区块编号和交易编号组成的二元组,存储也是按照键值对的方式进行的,键为“0x00”;值是二元组序列化后的结 果,GetLatestSavePoint是反序列化后得到的交易高度。具体的序列化和反序列化方法见后面的实现部分。
5.6.2 CouchDB
另外一个可选的数据库是CouchDB。CouchDB是一种文档型数据库,提供RESTful的API操作数据 库文档。CouchDB中的文档是无模式的(Schemaless),并不要求文档具有某种特定的结构。CouchDB支持原生的JSON和字节数组的操 作,基于JSON的操作,可以支持复杂的查询。如果存储的数据是字节数组,也支持基本的键值对操作。存储在CouchDB中的数据CouchDoc包含 JSONValue和附件两个部分,如下所示。
type CouchDoc struct {
JSONValue []byte
Attachments []Attachment
}
type Attachment struct {
Name string
ContentType string
Length uint64
AttachmentBytes []byte
}
其中JSONValue:也和存储的类型有关系,它最终会转换成一个JSON的结构。JSONValue结构序列化后的内容如下。
{
"version": "$BlockNum:$TxNum",
"chaincodeid": "$chaincodeID",
"data": "$rawJSON"
}
在超级账本中,如果存储的类型是JSON,且JSONValue的data字段是状态值经过JSON序列化后的内 容,则CouchDoc中的Attachments为空;如果存储的类型是字节数组,则JSONValue只保存版本信息,data字段为空,状态值放在 Attachments的AttachmentBytes中,Attachments的Name为"valueBytes",ContentType 为"application/octet-stream"。在获取时根据data字段是否为空可以判断出存储的状态值类型,最后得到存储的版本和状态值。
条件查询是基于LevelDB的状态数据库所没有的功能进行的。查询前会对查询条件进行转换,增加"data."前缀和查询记录限制等,查询结果还会默认增加"_id""version""chaincodeid"。比如原始的查询条件为:
{
"selector": {
"owner": {
"$eq": "tom"
}
},
"fields": [
"owner",
"asset_name",
"color",
"size"
],
"sort": [
"size",
"color"
]
}
转换后的查询条件为:
{
"selector": {
"$and": [
{
"chaincodeid": "marble"
},
{
"data.owner": {
"$eq": "tom"
}
}
]
},
"fields": [
"data.owner",
"data.asset_name",
"data.color",
"data.size",
"_id",
"version",
"chaincodeid"
],
"sort": [
"data.size",
"data.color"
],
"limit": 10,
"skip": 0
}
其中查询记录限制limit可以在ledger.state.queryLimit中设置,默认值为1000。
交易高度也是采用键值存储的,当键为statedb_savepoint,值为couchSavepointData时JSON的结构如下所示。
type couchSavepointData struct {
BlockNum uint64 `json:"BlockNum"`
TxNum uint64 `json:"TxNum"`
UpdateSeq string `json:"UpdateSeq"`
}
5.6.3 基于状态数据的区块验证
区块数据提交到账本前,会基于状态数据验证区块数据是否有效。区块验证之前会从区块中解析出有效载荷(详细的消息结构参见后面的章节),根据不同的类型分别进行验证,目前的区块类型有:
·背书交易区块
·配置交易区块
配置交易区块的验证目前暂时没有实现,验证结果都为成功。我们来看看背书交易区块的验证过程,交易区块的验证主要 是读写集的验证。从ChaincodeAction中解析出读写集TxRwSet,验证读取的版本是否和状态数据库里的版本一致。如果读写集中还有范围查 询,也会验证范围查询中的每个记录是否和状态数据库中的版本完全一致。验证通过的交易,其读写集会添加到UpdateBatch中,同一个区块的交易还会 验证是否会读取UpdateBatch中的记录,因为UpdateBatch中的记录是当前区块更新的数据,读取还没有提交到账本中的数据,这会导致和模 拟执行时读取的数据版本不一致,所以在这种情况下验证会失败。
区块提交到账本之后,要从区块数据中恢复状态数据和历史数据,就不会再对读写集进行版本检查。
数字签名信封,没有验证签名。
交易验证过程实现了一个验证字节图,每个交易占用一个字节,并标识其状态,定义如下:
type TxValidationFlags []uint8
验证后的字节图会存放在无数据中。验证成功的背书交易区块会生成读写集。
1.基于版本的验证
基于状态数据版本的验证过程比较简单,对每一个状态的kvRead.Key,比较状态数据里保存的版本是否和交易记录里面读取的版本一致,若完全一致就验证通过,表示在模拟执行之后这个kvRead.Key的值没有被修改过。
2.基于范围查询的验证
基于范围查询的验证方法是:比较“范围查询的结果”是否和在最新状态数据基础上更新本次交易数据后的“模拟状态数据”一致,有两种方法可以进行比较:
·基于默克尔树计算哈希值的比较;
·基于范围查询结果的查询。
如果范围查询的结果已经包含了默克尔树哈希计算结果,范围查询就采用基于默克尔树计算哈希值的比较方法。比较的过 程是逐个计算模拟状态数据的默克尔树哈希,在计算过程中判断是否和范围查询的默克尔树哈希值一致,若过程中出现不一致就退出。默克尔树的计算过程对新增元 素是友好的,可以在已经计算过的结果基础上迭代计算出最后的结果,不需要整个数组一起重新计算。
我们再来看看基于范围查询结果的查询比较过程,比较的方法就是遍历每个元素,比较两个迭代器元素的键值对是否完全一致,包括元素的个数。
5.7 历史数据
历史数据(History Database)记录了每个状态数据的历史信息,历史信息是保存在LevelDB数据库中的。每个历史信息用一个四元组(namespace、writeKey、blockNo、tranNo)来表示,其中:
·namespace:实际代表的是不同的chaincodeID,从这里也可以看出,不同chaincode的数据是逻辑隔离的;
·writeKey:要写入数据的键;
·blockNo:要写入数据所在的区块编号;
·tranNo:要写入数据所在区块内的交易序号,从0开始。
历史信息记录最细的粒度就是交易,如果在一个交易中多次对同一个writeKey更新数据,则会以第一次数据为准,历史信息实际存储的信息是固定的空字节数组[]byte{}。更新区块信息的时候,会同步更新检查点信息,保存的内容是最新的区块高度和最大的交易序号,检查点信息用来判断历史信息的状态是否是最新的。
5.8 数据恢复
区块的提交过程分为3个步骤:
·先保存区块到文件存储的账本数据中;
·然后更新状态数据;
·最后更新历史信息数据。
这3个步骤是顺序执行的,在这个过程中有文件的操作,也有数据库的操作,所以在任何一个步骤都可能出现错误或者中断。恢复的过程会根据账本数据记录的区块信息和状态数据、历史信息数据的检查点进行比较,重新提交检查点之后的区块信息,保持账本数据的一致性。
5.9 本章小结
本章介绍了Hyperledger Fabric 1.0的数据存储,包括账本数据(Ledger)、区块索引(Index)、状态数据(stale Database)、历史数据(History Database)等存储结构。目前的数据存储存在较大的优化空间,账本数据结构的设计带来的开销很大,最新的版本并没有实现账本裁剪(Ledger Prune)功能,不能进行归档,也不能删除无效交易,所以会导致存储空间持续增长。
来源:我是码农,转载请保留出处和链接!
本文链接:http://www.54manong.com/?id=1065
微信号:qq444848023 QQ号:444848023
加入【我是码农】QQ群:864689844(加群验证:我是码农)
全站首页 | 数据结构 | 区块链| 大数据 | 机器学习 | 物联网和云计算 | 面试笔试
var cnzz_protocol = (("https:" == document.location.protocol) ? "https://" : "http://");document.write(unescape("%3Cspan id='cnzz_stat_icon_1276413723'%3E%3C/span%3E%3Cscript src='" + cnzz_protocol + "s23.cnzz.com/z_stat.php%3Fid%3D1276413723%26show%3Dpic1' type='text/javascript'%3E%3C/script%3E"));本站资源大部分来自互联网,版权归原作者所有!
评论专区