超级账本的最终目的是将交易记录打包为区块保存到账本中,账本模块用来保存区块,检索区块,记录账本的最终状态。本节介绍了Peer账本的初始化过程。
1. 账本对象
Fabric的orderer会将交易信息打包为Block,Peer会对Block进行校验,然后保存起来,最后修改key的最终状态,在此过程中,还会记录历史信息。因此,对于一个Peer账本来说,需要完成以下功能:
- 使用Ledger维护整个账本,包括区块的校验,写入,查询
- 使用LedgerProvider维护账本的通用配置,最终为Channel生成Ledger
- 使用BlockStore保存区块数据
- 使用VersionedDBProvider维护最终状态
- 使用HistoryDBProvider维护历史记录
1.1 Ledger
在common/ledger/ledger_interface.go
中定义了账本Ledger的接口及其基本功能,从中我们可以对Ledger的作用有个概要了解。在Fabric中,一个Channel对应着一个账本,一个账本是包含一条由交易记录组成的区块链,以及交易导致的最终状态数据库和历史数据库。下面是Ledger的主要方法:
- GetBlockchainInfo() 获取当前账本的区块链信息,主要是区块链的高度,当前区块的Hash值及上一个区块的Hash值
- GetBlockByNumber(blockNumber) 返回指定编号的区块
- GetBlocksIterator(startBlockNumber) 获取一个从指定编号开始的迭代器,用于不断获取后续的区块
- Close() 关闭账本
- Commit(block) 提交区块
可以看出,Ledger主要用于提交区块,查询区块及区块链信息。其他对象中,ResultsIterator用于迭代器,可以不断查询下一个区块,QueryResult是查询结果,PrunePolicy是账本的修剪策略。
1.2 PeerLedger
PeerLedger在core/ledger/ledger_interface.go
中定义,实现了1.1 Ledger中的方法,并且添加了额外的其他方法。在common/ledger
中,主要是定义通用的Ledger,在core//ledger
中是实现。
PeerLedger类额外实现的功能主要是通过Block中的交易txid/hash值获取区块,初次之外,提供了TxSimulator交易模拟器,QueryExecutor查询器,HistoryQueryExecutor历史查询器等功能。
1.3 PeerLedgerProvider
PeerLedgerProvider 保存了账本的通用信息,用来创建/打开账本。例如,Provider里有保存账本的BlockStoreProvider,状态数据库和历史数据库的Provider,对于新创建的通道Channel1,PeerLedgerProvider为其返回一个PeerLedger实例,用来操作Channel1的账本。从下面的主要方法中就可以看出其功能:
- Create(genesisBlock) 使用创世块创建一个PeerLedger,创世块中包含了账本的相关配置信息
- Open(ledgerID) 根据账本ID返回一个PeerLedger,ledgerID就是Channel的名称
- Exists(ledgerID) 判断账本ID是否存在
- List() 列出当前的所有账本ID
PeerLedgerProvider中包含几个属性:
- 保存provider信息的数据库,使用leveldb保存provider的内容,在ledgerProvider文件夹中。
- blockStoreProvider 会生产BlockStore,定义如何保存区块,目前只有一种实现FSBlockSotre,其将区块数据保存到文件系统中。
- VersionedDBProvider 提供VersionDB的处理类,用于保存账本key值的最终状态。
- HistoryDBProvider 提供HistoryDB的处理类,用于保存key的历史记录。
2.账本初始化
使用peer start
启动peer节点时,首先会初始化与账本相关的对象,主要代码在ledger_mgmt.go
中,主要就是创建一个PeerLedgerProvider。
kvLedger是PeerLedger的实现类,由其创建PeerLedgerProvider,主要逻辑为:
- 获取
peer.fileSystemPath
配置的路径,在内部的ledgersData/ledgerProvider文件夹初始化一个leveldb的数据库,保存到idStore中。idStore用于维护账本id相关信息。 - 在保存区块的过程中,为了快速检索区块,需要建立索引,因此,创建了一个indexConfig,指定了需要建立索引的字段数组
- 生成blockStoreProvider,指定存储区块的路径,每个区块文件的最大大小,索引配置信息
- 初始化VersionedDBProvider,根据配置返回leveldb和couchdb的provider
- 初始化HistoryDBProvider
- 构造PeerLedgerProvider,准备返回
- 最后一步,查看是否存在创建账本时发生崩溃的账本id,如果存在的话,需要恢复,在Create()创建账本时会设置这个flag,内容为账本id,成功创建后会删除这个flag。因此如果中途程序崩溃,会留下这个flag。恢复的逻辑是重新创建KVLedger,使用blockStore从文件中获取blockchaininfo,如果高度为0,说明还没有提交创世块,删除flag即可,如果高度为1,已经提交创世块,使用idStore维护账本id,其他情况说明账本已经创建了。
3. idStore
idStore定义在kv_ledger_provider.go
中,内部包含一个用于操作leveldb的成员db,初始化账本的时候,会为provider创建一个leveldb数据库,主要功能:
- underConstructionFlag 操作underConstructionLedgerKey,这个key用来记录账本创建过程中是否发生了异常,发生异常时会有补偿机制重建账本。
- 在PeerLedgerProvider使用
Create(genesisBlock *common.Block)
创建PeerLedger时,会将账本的id和创世块内容组成键值对保存在数据库中。 - 获取所有的账本id,遍历数据库的key,如果是账本id的前缀,则说明有这个账本,存放在返回的list中。
4.BlockStore
BlockStore定义在common/ledger/blkstorage/blockstorage.go
中,只有一个实现fsblkstorage,在文件系统中保存区块。这是账本机制的重要组成部分,PeerLedger的实现kvLedger中的大部分操作都是使用的BlockStore,VersionedDB和HistroyDB。
下面是BlockStore接口的方法,可以看到,主要是添加取款,获取区块链信息,检索区块的功能。
type BlockStore interface {
AddBlock(block *common.Block) error
GetBlockchainInfo() (*common.BlockchainInfo, error)
RetrieveBlocks(startNum uint64) (ledger.ResultsIterator, error)
......
Shutdown()
}
fs_blockstore.go
中的fsBlockStore实现了BlockStore,在文件系统中保存区块。需要注意的是fsBlockStore中有一个blockfileMgr,里面负责具体的文件操作。
4.1 blockfileMgr
blockfileMgr定义在blockfile_mgr.go
中,用来管理将Block写入文件,建立索引,获取Block。blockfileMgr中有如下几个重要的组件,用来帮助完成存储Block的操作。
blockfileWriter
blockfileWriter用于将数据写入文件,由于fsBlockStore将区块保存到了多个文件中,blockfileWriter记录了写入文件的文件夹和操作当前文件的os.File
checkpointinfo
checkpointInfo定义在blockfile_mgr.go
中,用来记录当前最后一个文件的序号,已写入文件的字节数,最后一个Block的编号信息。checkpointinfo会被保存在数据库中,每次初始化时,可以从数据库中读出,更新Block时,需要更新checkpointinfo并更新数据库。
BlockchainInfo
BlockchainInfo记录了区块链的状态,如高度,最后一个Block的Hash等
blockIndex
blockIndex用来建立索引,indexBlock
4.2 提交Block
blockfileMgr的addBlock负责添加Block,主要逻辑为:
- 校验区块编号,必须与BlockChain的高度相同,例如:区块链现有3个区块,编号分别为0-1-2,高度为3,那么新添加的Block编号需要是3。
- 使用protobuf将Block序列化,Block的结构为
common.Block
- BlockHeader
- Number > 区块编号
- PrevHash > 上一个区块的Hash
- DataHash > BlockData的Hash
- BlockData
- txEnvelope > 一个交易
- Payload
- Header > 头
- ChannelHeader > 类型 版本和ChannelID
- SignatureHeader > 签名头
- Signature
- Creator > 创建者
- Nonce > 随机数
- BlockMetadata
在序列化的过程中,可以提取出Block的交易id列表,元数据等信息,保存在serializedBlockInfo中;还可以计算出区块占用的字节数。
- 计算Block的Hash值,逻辑为将Block的编号,上一个Block的Hash和当前区块的DataHash使用ASN.1 编码为byte[],然后再使用SHA-256生成当前区块的Hash值。
- 将Block的byte数组(记为A)写入文件,写入内容为B:A的长度+A的内容,那么B的大小就是要写入文件的总大小,由于每个文件有大小限制,如果剩余的文件容量不足以支持写入,就重新创建一个文件并写入。blockfileMgr中维护了checkpointinfo,内部记录了账本当前所处于的文件及offset,每次写入之后,都会修改checkpointinfo信息。
- 在写入数据期间,如果发生error,会将文件恢复到写入之前的状态,并抛出异常。
- 更新checkpointInfo的信息,并保存到数据库中。
- 写入成功后,使用blockIndex,创建索引,这样可以根据block编号,hash值,交易id快速定位到Block所在文件的位置
- 添加Block后,BlockChainInfo最后一个Block发生变化,更新这些信息,。